mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-27 17:45:52 -05:00
Merge pull request #5451 from paulkaplan/studio-alerts
Alert component and studio error and success alerts
This commit is contained in:
commit
832324ea83
21 changed files with 500 additions and 187 deletions
41
src/components/alert/alert-component.jsx
Normal file
41
src/components/alert/alert-component.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import Button from '../../components/forms/button.jsx';
|
||||
|
||||
import './alert.scss';
|
||||
|
||||
const AlertComponent = ({className, icon, id, values, onClear}) => (
|
||||
<div className="alert-wrapper">
|
||||
<div
|
||||
className={classNames('alert', className)}
|
||||
>
|
||||
{icon && <img
|
||||
className="alert-icon"
|
||||
src={icon}
|
||||
/>}
|
||||
<div className="alert-msg">
|
||||
<FormattedMessage
|
||||
id={id}
|
||||
values={values}
|
||||
/>
|
||||
</div>
|
||||
{onClear && <Button
|
||||
className="alert-close-button"
|
||||
isCloseType
|
||||
onClick={onClear}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
AlertComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
values: PropTypes.shape({}),
|
||||
onClear: PropTypes.func
|
||||
};
|
||||
|
||||
export default AlertComponent;
|
18
src/components/alert/alert-context.js
Normal file
18
src/components/alert/alert-context.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {createContext, useContext} from 'react';
|
||||
import AlertStatus from './alert-status.js';
|
||||
|
||||
const AlertContext = createContext({
|
||||
// Note: defaults here are only used if there is no Provider in the tree
|
||||
status: AlertStatus.NONE,
|
||||
data: {},
|
||||
clearAlert: () => {},
|
||||
successAlert: () => {},
|
||||
errorAlert: () => {}
|
||||
});
|
||||
|
||||
const useAlertContext = () => useContext(AlertContext);
|
||||
|
||||
export {
|
||||
AlertContext as default,
|
||||
useAlertContext
|
||||
};
|
54
src/components/alert/alert-provider.jsx
Normal file
54
src/components/alert/alert-provider.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import React, {useRef, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import AlertStatus from './alert-status.js';
|
||||
import AlertContext from './alert-context.js';
|
||||
|
||||
const AlertProvider = ({children}) => {
|
||||
const defaultState = {
|
||||
status: AlertStatus.NONE,
|
||||
data: {},
|
||||
showClear: false
|
||||
};
|
||||
const [state, setState] = useState(defaultState);
|
||||
const timeoutRef = useRef(null);
|
||||
|
||||
const clearAlert = () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
setState(defaultState);
|
||||
};
|
||||
|
||||
const handleAlert = (status, data, timeoutSeconds = 3) => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
setState({status, data, showClear: !timeoutSeconds});
|
||||
if (timeoutSeconds) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
timeoutRef.current = null;
|
||||
setState(defaultState);
|
||||
}, timeoutSeconds * 1000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertContext.Provider
|
||||
value={{
|
||||
status: state.status,
|
||||
data: state.data,
|
||||
showClear: state.showClear,
|
||||
clearAlert: clearAlert,
|
||||
successAlert: (newData, timeoutSeconds = 3) =>
|
||||
handleAlert(AlertStatus.SUCCESS, newData, timeoutSeconds),
|
||||
errorAlert: (newData, timeoutSeconds = 3) =>
|
||||
handleAlert(AlertStatus.ERROR, newData, timeoutSeconds)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
};
|
||||
AlertProvider.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default AlertProvider;
|
5
src/components/alert/alert-status.js
Normal file
5
src/components/alert/alert-status.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
NONE: 'NONE',
|
||||
SUCCESS: 'SUCCESS',
|
||||
ERROR: 'ERROR'
|
||||
};
|
33
src/components/alert/alert.jsx
Normal file
33
src/components/alert/alert.jsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AlertComponent from './alert-component.jsx';
|
||||
import AlertStatus from './alert-status.js';
|
||||
import {useAlertContext} from './alert-context.js';
|
||||
|
||||
import successIcon from './icon-alert-success.svg';
|
||||
import errorIcon from './icon-alert-error.svg';
|
||||
|
||||
const Alert = ({className}) => {
|
||||
const {status, data, showClear, clearAlert} = useAlertContext();
|
||||
if (status === AlertStatus.NONE) return null;
|
||||
return (
|
||||
<AlertComponent
|
||||
className={classNames(className, {
|
||||
'alert-success': status === AlertStatus.SUCCESS,
|
||||
'alert-error': status === AlertStatus.ERROR
|
||||
})}
|
||||
icon={status === AlertStatus.SUCCESS ? successIcon : errorIcon}
|
||||
id={data.id}
|
||||
values={data.values}
|
||||
onClear={showClear && clearAlert}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.propTypes = {
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
export default Alert;
|
40
src/components/alert/alert.scss
Normal file
40
src/components/alert/alert.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
|
||||
.alert-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
|
||||
.alert {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
min-height: 60px;
|
||||
pointer-events: auto;
|
||||
&.alert-error {
|
||||
background: #FFF0DF;
|
||||
border: 1px solid #FF8C1A;
|
||||
box-shadow: 0px 0px 0px 2px rgba(255, 140, 26, 0.25)
|
||||
}
|
||||
|
||||
&.alert-success {
|
||||
background: #CEF2E8;
|
||||
border: 1px solid #0EBD8C;
|
||||
box-shadow: 0px 0px 0px rgba(14, 189, 140, 0.25);
|
||||
}
|
||||
.alert-msg {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.alert-close-button {
|
||||
position: unset;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
3
src/components/alert/icon-alert-error.svg
Normal file
3
src/components/alert/icon-alert-error.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="28" height="20" viewBox="-2 -1 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.40571 6.50912C1.50472 6.50912 0.775271 7.23857 0.775271 8.13956C0.775271 9.04055 1.50472 9.77 2.40571 9.77C3.3067 9.77 4.03615 9.04055 4.03615 8.13956C4.03615 7.23857 3.3067 6.50912 2.40571 6.50912ZM3.34168 5.02359C2.92699 5.9523 1.88444 5.9523 1.46975 5.02359L0.145744 2.07519C-0.268945 1.15289 0.250665 0 1.08171 0H3.72972C4.56076 0 5.08037 1.15289 4.66568 2.07519L3.34168 5.02359Z" fill="#FF8C1A"/>
|
||||
</svg>
|
After Width: | Height: | Size: 559 B |
9
src/components/alert/icon-alert-success.svg
Normal file
9
src/components/alert/icon-alert-success.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg width="28" height="20" viewBox="0 0 28 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86144 15.403C7.43527 15.403 7.0091 15.2398 6.68447 14.9152L3.48818 11.7189C2.83727 11.068 2.83727 10.0159 3.48818 9.36498C4.13909 8.71407 5.19121 8.71407 5.84212 9.36498L7.86144 11.3843L14.1591 5.08828C14.8084 4.43737 15.8622 4.43737 16.5131 5.08828C17.1623 5.73753 17.1623 6.7913 16.5131 7.44222L9.03841 14.9152C8.71378 15.2398 8.28761 15.403 7.86144 15.403Z" fill="#575E75"/>
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="3" y="4" width="14" height="12">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.86144 15.403C7.43527 15.403 7.0091 15.2398 6.68447 14.9152L3.48818 11.7189C2.83727 11.068 2.83727 10.0159 3.48818 9.36498C4.13909 8.71407 5.19121 8.71407 5.84212 9.36498L7.86144 11.3843L14.1591 5.08828C14.8084 4.43737 15.8622 4.43737 16.5131 5.08828C17.1623 5.73753 17.1623 6.7913 16.5131 7.44222L9.03841 14.9152C8.71378 15.2398 8.28761 15.403 7.86144 15.403Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<rect width="20" height="20" fill="#0FBD8C"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -14,12 +14,43 @@ const SubNavigation = require('../../components/subnavigation/subnavigation.jsx'
|
|||
const Select = require('../../components/forms/select.jsx');
|
||||
const OverflowMenu = require('../../components/overflow-menu/overflow-menu.jsx').default;
|
||||
const exampleIcon = require('./example-icon.svg');
|
||||
const AlertProvider = require('../../components/alert/alert-provider.jsx').default;
|
||||
const {useAlertContext} = require('../../components/alert/alert-context.js');
|
||||
const Alert = require('../../components/alert/alert.jsx').default;
|
||||
|
||||
require('./components.scss');
|
||||
|
||||
/* eslint-disable react/prop-types, react/jsx-no-bind */
|
||||
/* Demo of how to use the useAlertContext hook */
|
||||
const AlertButton = ({type, timeoutSeconds}) => {
|
||||
const {errorAlert, successAlert} = useAlertContext();
|
||||
const onClick = type === 'success' ?
|
||||
() => successAlert({id: 'success-alert.string.id'}, timeoutSeconds) :
|
||||
() => errorAlert({id: 'error-alert.string.id'}, timeoutSeconds);
|
||||
return (
|
||||
<Button onClick={onClick}>
|
||||
{type}, {timeoutSeconds || 'no '} timeout
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const Components = () => (
|
||||
<div className="components">
|
||||
<div className="inner">
|
||||
<h1>Alert Provider, Display and Hooks</h1>
|
||||
<AlertProvider>
|
||||
<div style={{position: 'relative', minHeight: '200px', border: '1px solid red'}}>
|
||||
<Alert />
|
||||
<div><AlertButton
|
||||
type="success"
|
||||
timeoutSeconds={3}
|
||||
/></div>
|
||||
<div><AlertButton
|
||||
type="error"
|
||||
timeoutSeconds={null}
|
||||
/></div>
|
||||
</div>
|
||||
</AlertProvider>
|
||||
<h1>Overflow Menu</h1>
|
||||
<div className="example-tile">
|
||||
<OverflowMenu>
|
||||
|
|
|
@ -77,5 +77,15 @@
|
|||
"studio.reportPleaseExplain": "Please select which part of the studio you find to be disrespectful or inappropriate, or otherwise breaks the Scratch Community Guidelines.",
|
||||
"studio.reportAreThereComments": "Are there inappropriate comments in the studio? Please report them by clicking the \"report\" button on the individual comments.",
|
||||
"studio.reportThanksForLettingUsKnow": "Thanks for letting us know!",
|
||||
"studio.reportYourFeedback": "Your feedback will help us make Scratch better."
|
||||
"studio.reportYourFeedback": "Your feedback will help us make Scratch better.",
|
||||
|
||||
"studio.alertProjectAdded": "\"{title}\" added to studio",
|
||||
"studio.alertProjectAlreadyAdded": "That project is already in this studio",
|
||||
"studio.alertProjectRemoveError": "Something went wrong removing the project",
|
||||
"studio.alertProjectAddError": "Something went wrong adding the project",
|
||||
"studio.alertCuratorAlreadyInvited": "\"{name}\" has already been invited",
|
||||
"studio.alertCuratorInvited": "Curator invite sent to \"{name}\"",
|
||||
"studio.alertManagerPromote": "\"{name}\" is now a manager",
|
||||
"studio.alertManagerPromoteError": "Something went wrong promoting \"{name}\"",
|
||||
"studio.alertMemberRemoveError": "Something went wrong removing \"{name}\""
|
||||
}
|
||||
|
|
|
@ -121,8 +121,6 @@ const inviteCurator = username => ((dispatch, getState) => new Promise((resolve,
|
|||
}, (err, body, res) => {
|
||||
const error = normalizeError(err, body, res);
|
||||
if (error) return reject(error);
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(`successfully invited ${username}`);
|
||||
return resolve(username);
|
||||
});
|
||||
}));
|
||||
|
|
|
@ -18,6 +18,8 @@ import UserProjectsTile from './user-projects-tile.jsx';
|
|||
|
||||
import './user-projects-modal.scss';
|
||||
import {selectIsEducator} from '../../../redux/session';
|
||||
import AlertProvider from '../../../components/alert/alert-provider.jsx';
|
||||
import Alert from '../../../components/alert/alert.jsx';
|
||||
|
||||
const UserProjectsModal = ({
|
||||
items, error, loading, moreToLoad, showStudentsFilter,
|
||||
|
@ -72,32 +74,35 @@ const UserProjectsModal = ({
|
|||
}
|
||||
</SubNavigation>
|
||||
<ModalInnerContent className="user-projects-modal-content">
|
||||
{error && <div>Error loading {filter}: {error}</div>}
|
||||
<div className="user-projects-modal-grid">
|
||||
{items.map(project => (
|
||||
<UserProjectsTile
|
||||
key={project.id}
|
||||
id={project.id}
|
||||
title={project.title}
|
||||
image={project.image}
|
||||
inStudio={project.inStudio}
|
||||
onAdd={onAdd}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
{moreToLoad &&
|
||||
<div className="studio-projects-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={() => onLoadMore(filter)}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
<AlertProvider>
|
||||
{error && <div>Error loading {filter}: {error}</div>}
|
||||
<Alert className="studio-alert" />
|
||||
<div className="user-projects-modal-grid">
|
||||
{items.map(project => (
|
||||
<UserProjectsTile
|
||||
key={project.id}
|
||||
id={project.id}
|
||||
title={project.title}
|
||||
image={project.image}
|
||||
inStudio={project.inStudio}
|
||||
onAdd={onAdd}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
{moreToLoad &&
|
||||
<div className="studio-projects-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={() => onLoadMore(filter)}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</AlertProvider>
|
||||
</ModalInnerContent>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import React, {useContext, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import AlertContext from '../../../components/alert/alert-context.js';
|
||||
|
||||
const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [added, setAdded] = useState(inStudio);
|
||||
const [error, setError] = useState(null);
|
||||
const {errorAlert} = useContext(AlertContext);
|
||||
const toggle = () => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
(added ? onRemove(id) : onAdd(id))
|
||||
.then(() => {
|
||||
setAdded(!added);
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch(e => {
|
||||
setError(e);
|
||||
.catch(() => {
|
||||
setSubmitting(false);
|
||||
errorAlert({
|
||||
id: added ? 'studio.alertProjectRemoveError' :
|
||||
'studio.alertProjectAddError'
|
||||
}, null);
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
@ -45,7 +48,6 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
|
|||
<div className={`studio-tile-dynamic-${added ? 'remove' : 'add'}`}>
|
||||
{added ? '✔' : '+'}
|
||||
</div>
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@ import {connect} from 'react-redux';
|
|||
import classNames from 'classnames';
|
||||
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
|
||||
|
||||
import {useAlertContext} from '../../components/alert/alert-context';
|
||||
import {Errors, inviteCurator} from './lib/studio-member-actions';
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
|
||||
|
@ -24,12 +25,30 @@ const StudioCuratorInviter = ({intl, onSubmit}) => {
|
|||
const [value, setValue] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const {successAlert} = useAlertContext();
|
||||
|
||||
const submit = () => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => {
|
||||
successAlert({
|
||||
id: 'studio.alertCuratorInvited',
|
||||
values: {name: value}
|
||||
});
|
||||
setValue('');
|
||||
})
|
||||
.catch(e => {
|
||||
if (e === Errors.DUPLICATE) {
|
||||
successAlert({
|
||||
id: 'studio.alertCuratorAlreadyInvited',
|
||||
values: {name: value}
|
||||
});
|
||||
setValue('');
|
||||
} else {
|
||||
setError(e);
|
||||
}
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -11,6 +11,8 @@ import CuratorInviter from './studio-curator-inviter.jsx';
|
|||
import CuratorInvite from './studio-curator-invite.jsx';
|
||||
import {loadCurators} from './lib/studio-member-actions';
|
||||
import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions';
|
||||
import AlertProvider from '../../components/alert/alert-provider.jsx';
|
||||
import Alert from '../../components/alert/alert.jsx';
|
||||
|
||||
const StudioCurators = ({
|
||||
canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore
|
||||
|
@ -19,61 +21,65 @@ const StudioCurators = ({
|
|||
if (items.length === 0) onLoadMore();
|
||||
}, []);
|
||||
|
||||
return (<div className="studio-members">
|
||||
<div className="studio-header-container">
|
||||
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
|
||||
</div>
|
||||
{canInviteCurators && <CuratorInviter />}
|
||||
{showCuratorInvite && <CuratorInvite />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-members-grid">
|
||||
{items.length === 0 && !loading ? (
|
||||
<div className="studio-empty">
|
||||
<img
|
||||
width="179"
|
||||
height="111"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/curators-empty.png"
|
||||
/>
|
||||
{canInviteCurators ? (
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.curatorsEmptyCanAdd1" /></div>
|
||||
<div><FormattedMessage id="studio.curatorsEmptyCanAdd2" /></div>
|
||||
return (
|
||||
<AlertProvider>
|
||||
<div className="studio-members">
|
||||
<Alert className="studio-alert" />
|
||||
<div className="studio-header-container">
|
||||
<h2><FormattedMessage id="studio.curatorsHeader" /></h2>
|
||||
</div>
|
||||
{canInviteCurators && <CuratorInviter />}
|
||||
{showCuratorInvite && <CuratorInvite />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-members-grid">
|
||||
{items.length === 0 && !loading ? (
|
||||
<div className="studio-empty">
|
||||
<img
|
||||
width="179"
|
||||
height="111"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/curators-empty.png"
|
||||
/>
|
||||
{canInviteCurators ? (
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.curatorsEmptyCanAdd1" /></div>
|
||||
<div><FormattedMessage id="studio.curatorsEmptyCanAdd2" /></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.curatorsEmpty1" /></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.curatorsEmpty1" /></div>
|
||||
</div>
|
||||
<React.Fragment>
|
||||
{items.map(item =>
|
||||
(<CuratorTile
|
||||
key={item.username}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<div className="studio-members-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{items.map(item =>
|
||||
(<CuratorTile
|
||||
key={item.username}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<div className="studio-members-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'mod-mutating': loading
|
||||
})}
|
||||
onClick={onLoadMore}
|
||||
>
|
||||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
</div>
|
||||
</AlertProvider>);
|
||||
};
|
||||
|
||||
StudioCurators.propTypes = {
|
||||
|
|
|
@ -8,6 +8,8 @@ import {managers} from './lib/redux-modules';
|
|||
import {loadManagers} from './lib/studio-member-actions';
|
||||
import Debug from './debug.jsx';
|
||||
import {ManagerTile} from './studio-member-tile.jsx';
|
||||
import AlertProvider from '../../components/alert/alert-provider.jsx';
|
||||
import Alert from '../../components/alert/alert.jsx';
|
||||
|
||||
|
||||
const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
|
||||
|
@ -16,24 +18,26 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="studio-members">
|
||||
<div className="studio-header-container">
|
||||
<h2><FormattedMessage id="studio.managersHeader" /></h2>
|
||||
</div>
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-members-grid">
|
||||
{items.map(item =>
|
||||
(<ManagerTile
|
||||
key={item.username}
|
||||
id={item.id}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<AlertProvider>
|
||||
<div className="studio-members">
|
||||
<Alert className="studio-alert" />
|
||||
<div className="studio-header-container">
|
||||
<h2><FormattedMessage id="studio.managersHeader" /></h2>
|
||||
</div>
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-members-grid">
|
||||
{items.map(item =>
|
||||
(<ManagerTile
|
||||
key={item.username}
|
||||
id={item.id}
|
||||
username={item.username}
|
||||
image={item.profile.images['90x90']}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<div className="studio-members-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
|
@ -44,9 +48,10 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
|
|||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
removeCurator,
|
||||
removeManager
|
||||
} from './lib/studio-member-actions';
|
||||
import {useAlertContext} from '../../components/alert/alert-context';
|
||||
|
||||
import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx';
|
||||
import removeIcon from './icons/remove-icon.svg';
|
||||
|
@ -23,7 +24,7 @@ const StudioMemberTile = ({
|
|||
username, image // own props
|
||||
}) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const {errorAlert, successAlert} = useAlertContext();
|
||||
const userUrl = `/users/${username}`;
|
||||
return (
|
||||
<div className="studio-member-tile">
|
||||
|
@ -50,11 +51,20 @@ const StudioMemberTile = ({
|
|||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onPromote(username).catch(e => {
|
||||
setError(e);
|
||||
setSubmitting(false);
|
||||
});
|
||||
onPromote(username)
|
||||
.then(() => {
|
||||
successAlert({
|
||||
id: 'studio.alertManagerPromote',
|
||||
values: {name: username}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
errorAlert({
|
||||
id: 'studio.alertManagerPromoteError',
|
||||
values: {name: username}
|
||||
});
|
||||
setSubmitting(false);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img src={promoteIcon} />
|
||||
|
@ -69,9 +79,11 @@ const StudioMemberTile = ({
|
|||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onRemove(username).catch(e => {
|
||||
setError(e);
|
||||
onRemove(username).catch(() => {
|
||||
errorAlert({
|
||||
id: 'studio.alertMemberRemoveError',
|
||||
values: {name: username}
|
||||
}, null);
|
||||
setSubmitting(false);
|
||||
});
|
||||
}}
|
||||
|
@ -82,7 +94,6 @@ const StudioMemberTile = ({
|
|||
</li>}
|
||||
</OverflowMenu>
|
||||
}
|
||||
{error && <div>{error}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
|
|||
import {Errors, addProject} from './lib/studio-project-actions';
|
||||
import UserProjectsModal from './modals/user-projects-modal.jsx';
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
import {useAlertContext} from '../../components/alert/alert-context';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
|
@ -26,12 +27,28 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
|
|||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const {successAlert} = useAlertContext();
|
||||
const submit = () => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onSubmit(value)
|
||||
.then(() => setValue(''))
|
||||
.catch(e => setError(e))
|
||||
.then(() => {
|
||||
successAlert({
|
||||
id: 'studio.alertProjectAdded',
|
||||
values: {title: value}
|
||||
});
|
||||
setValue('');
|
||||
})
|
||||
.catch(e => {
|
||||
// Duplicate project will show success alert
|
||||
if (e === Errors.DUPLICATE) {
|
||||
successAlert({id: 'studio.alertProjectAlreadyAdded'});
|
||||
setValue('');
|
||||
} else {
|
||||
// Other errors are displayed by this component
|
||||
setError(e);
|
||||
}
|
||||
})
|
||||
.then(() => setSubmitting(false));
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, {useState} from 'react';
|
||||
import React, {useContext, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import AlertContext from '../../components/alert/alert-context.js';
|
||||
import {selectCanRemoveProject} from '../../redux/studio-permissions';
|
||||
import {removeProject} from './lib/studio-project-actions';
|
||||
|
||||
|
@ -16,9 +17,9 @@ const StudioProjectTile = ({
|
|||
id, title, image, avatar, username // own props
|
||||
}) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const projectUrl = `/projects/${id}`;
|
||||
const userUrl = `/users/${username}`;
|
||||
const {errorAlert} = useContext(AlertContext);
|
||||
return (
|
||||
<div className="studio-project-tile">
|
||||
<a href={projectUrl}>
|
||||
|
@ -54,11 +55,10 @@ const StudioProjectTile = ({
|
|||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
onRemove(id)
|
||||
.catch(e => {
|
||||
setError(e);
|
||||
.catch(() => {
|
||||
setSubmitting(false);
|
||||
errorAlert({id: 'studio.alertProjectRemoveError'}, null);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
@ -67,7 +67,6 @@ const StudioProjectTile = ({
|
|||
</button></li>
|
||||
</OverflowMenu>
|
||||
}
|
||||
{error && <div>{error}</div>} {/* TODO where do these errors go? */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,8 @@ import StudioProjectAdder from './studio-project-adder.jsx';
|
|||
import StudioProjectTile from './studio-project-tile.jsx';
|
||||
import {loadProjects} from './lib/studio-project-actions.js';
|
||||
import classNames from 'classnames';
|
||||
import AlertProvider from '../../components/alert/alert-provider.jsx';
|
||||
import Alert from '../../components/alert/alert.jsx';
|
||||
|
||||
const StudioProjects = ({
|
||||
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore
|
||||
|
@ -20,62 +22,64 @@ const StudioProjects = ({
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="studio-projects">
|
||||
<div className="studio-header-container">
|
||||
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
|
||||
{canEditOpenToAll && <StudioOpenToAll />}
|
||||
</div>
|
||||
{canAddProjects && <StudioProjectAdder />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-projects-grid">
|
||||
{items.length === 0 && !loading ? (
|
||||
<div className="studio-empty">
|
||||
{canAddProjects ? (
|
||||
<React.Fragment>
|
||||
<img
|
||||
width="388"
|
||||
height="265"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/projects-empty-can-add.png"
|
||||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<img
|
||||
width="186"
|
||||
height="138"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/projects-empty.png"
|
||||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmpty1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmpty2" /></div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{items.map(item =>
|
||||
(<StudioProjectTile
|
||||
fetching={loading}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
image={item.image}
|
||||
avatar={item.avatar['90x90']}
|
||||
username={item.username}
|
||||
addedBy={item.actor_id}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<AlertProvider>
|
||||
<div className="studio-projects">
|
||||
<Alert className="studio-alert" />
|
||||
<div className="studio-header-container">
|
||||
<h2><FormattedMessage id="studio.projectsHeader" /></h2>
|
||||
{canEditOpenToAll && <StudioOpenToAll />}
|
||||
</div>
|
||||
{canAddProjects && <StudioProjectAdder />}
|
||||
{error && <Debug
|
||||
label="Error"
|
||||
data={error}
|
||||
/>}
|
||||
<div className="studio-projects-grid">
|
||||
{items.length === 0 && !loading ? (
|
||||
<div className="studio-empty">
|
||||
{canAddProjects ? (
|
||||
<React.Fragment>
|
||||
<img
|
||||
width="388"
|
||||
height="265"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/projects-empty-can-add.png"
|
||||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<img
|
||||
width="186"
|
||||
height="138"
|
||||
className="studio-empty-img"
|
||||
src="/images/studios/projects-empty.png"
|
||||
/>
|
||||
<div className="studio-empty-msg">
|
||||
<div><FormattedMessage id="studio.projectsEmpty1" /></div>
|
||||
<div><FormattedMessage id="studio.projectsEmpty2" /></div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{items.map(item =>
|
||||
(<StudioProjectTile
|
||||
fetching={loading}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
image={item.image}
|
||||
avatar={item.avatar['90x90']}
|
||||
username={item.username}
|
||||
addedBy={item.actor_id}
|
||||
/>)
|
||||
)}
|
||||
{moreToLoad &&
|
||||
<div className="studio-projects-load-more">
|
||||
<button
|
||||
className={classNames('button', {
|
||||
|
@ -86,11 +90,12 @@ const StudioProjects = ({
|
|||
<FormattedMessage id="general.loadMore" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -113,7 +113,10 @@ $radius: 8px;
|
|||
.active > li { background: $ui-blue; }
|
||||
}
|
||||
|
||||
.studio-projects {}
|
||||
.studio-projects, .studio-members {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.studio-projects-grid {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
|
@ -193,7 +196,6 @@ $radius: 8px;
|
|||
}
|
||||
}
|
||||
|
||||
.studio-members {}
|
||||
.studio-members-grid {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
|
|
Loading…
Reference in a new issue