Merge pull request #5451 from paulkaplan/studio-alerts

Alert component and studio error and success alerts
This commit is contained in:
Paul Kaplan 2021-05-24 15:33:10 -04:00 committed by GitHub
commit 832324ea83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 500 additions and 187 deletions

View 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;

View 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
};

View 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;

View file

@ -0,0 +1,5 @@
export default {
NONE: 'NONE',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR'
};

View 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;

View 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;
}
}
}

View 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

View 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

View file

@ -14,12 +14,43 @@ const SubNavigation = require('../../components/subnavigation/subnavigation.jsx'
const Select = require('../../components/forms/select.jsx'); const Select = require('../../components/forms/select.jsx');
const OverflowMenu = require('../../components/overflow-menu/overflow-menu.jsx').default; const OverflowMenu = require('../../components/overflow-menu/overflow-menu.jsx').default;
const exampleIcon = require('./example-icon.svg'); 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'); 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 = () => ( const Components = () => (
<div className="components"> <div className="components">
<div className="inner"> <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> <h1>Overflow Menu</h1>
<div className="example-tile"> <div className="example-tile">
<OverflowMenu> <OverflowMenu>

View file

@ -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.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.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.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}\""
} }

View file

@ -121,8 +121,6 @@ const inviteCurator = username => ((dispatch, getState) => new Promise((resolve,
}, (err, body, res) => { }, (err, body, res) => {
const error = normalizeError(err, body, res); const error = normalizeError(err, body, res);
if (error) return reject(error); if (error) return reject(error);
// eslint-disable-next-line no-alert
alert(`successfully invited ${username}`);
return resolve(username); return resolve(username);
}); });
})); }));

View file

@ -18,6 +18,8 @@ import UserProjectsTile from './user-projects-tile.jsx';
import './user-projects-modal.scss'; import './user-projects-modal.scss';
import {selectIsEducator} from '../../../redux/session'; import {selectIsEducator} from '../../../redux/session';
import AlertProvider from '../../../components/alert/alert-provider.jsx';
import Alert from '../../../components/alert/alert.jsx';
const UserProjectsModal = ({ const UserProjectsModal = ({
items, error, loading, moreToLoad, showStudentsFilter, items, error, loading, moreToLoad, showStudentsFilter,
@ -72,32 +74,35 @@ const UserProjectsModal = ({
} }
</SubNavigation> </SubNavigation>
<ModalInnerContent className="user-projects-modal-content"> <ModalInnerContent className="user-projects-modal-content">
{error && <div>Error loading {filter}: {error}</div>} <AlertProvider>
<div className="user-projects-modal-grid"> {error && <div>Error loading {filter}: {error}</div>}
{items.map(project => ( <Alert className="studio-alert" />
<UserProjectsTile <div className="user-projects-modal-grid">
key={project.id} {items.map(project => (
id={project.id} <UserProjectsTile
title={project.title} key={project.id}
image={project.image} id={project.id}
inStudio={project.inStudio} title={project.title}
onAdd={onAdd} image={project.image}
onRemove={onRemove} inStudio={project.inStudio}
/> onAdd={onAdd}
))} onRemove={onRemove}
{moreToLoad && />
<div className="studio-projects-load-more"> ))}
<button {moreToLoad &&
className={classNames('button', { <div className="studio-projects-load-more">
'mod-mutating': loading <button
})} className={classNames('button', {
onClick={() => onLoadMore(filter)} 'mod-mutating': loading
> })}
<FormattedMessage id="general.loadMore" /> onClick={() => onLoadMore(filter)}
</button> >
<FormattedMessage id="general.loadMore" />
</button>
</div>
}
</div> </div>
} </AlertProvider>
</div>
</ModalInnerContent> </ModalInnerContent>
</Modal> </Modal>
); );

View file

@ -1,23 +1,26 @@
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react'; import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import AlertContext from '../../../components/alert/alert-context.js';
const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => { const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [added, setAdded] = useState(inStudio); const [added, setAdded] = useState(inStudio);
const [error, setError] = useState(null); const {errorAlert} = useContext(AlertContext);
const toggle = () => { const toggle = () => {
setSubmitting(true); setSubmitting(true);
setError(null);
(added ? onRemove(id) : onAdd(id)) (added ? onRemove(id) : onAdd(id))
.then(() => { .then(() => {
setAdded(!added); setAdded(!added);
setSubmitting(false); setSubmitting(false);
}) })
.catch(e => { .catch(() => {
setError(e);
setSubmitting(false); setSubmitting(false);
errorAlert({
id: added ? 'studio.alertProjectRemoveError' :
'studio.alertProjectAddError'
}, null);
}); });
}; };
return ( return (
@ -45,7 +48,6 @@ const UserProjectsTile = ({id, title, image, inStudio, onAdd, onRemove}) => {
<div className={`studio-tile-dynamic-${added ? 'remove' : 'add'}`}> <div className={`studio-tile-dynamic-${added ? 'remove' : 'add'}`}>
{added ? '✔' : ''} {added ? '✔' : ''}
</div> </div>
{error && <div>{error}</div>}
</div> </div>
</div> </div>
); );

View file

@ -5,6 +5,7 @@ import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage, intlShape, injectIntl} from 'react-intl'; import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {useAlertContext} from '../../components/alert/alert-context';
import {Errors, inviteCurator} from './lib/studio-member-actions'; import {Errors, inviteCurator} from './lib/studio-member-actions';
import ValidationMessage from '../../components/forms/validation-message.jsx'; import ValidationMessage from '../../components/forms/validation-message.jsx';
@ -24,12 +25,30 @@ const StudioCuratorInviter = ({intl, onSubmit}) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const {successAlert} = useAlertContext();
const submit = () => { const submit = () => {
setSubmitting(true); setSubmitting(true);
setError(null); setError(null);
onSubmit(value) onSubmit(value)
.then(() => setValue('')) .then(() => {
.catch(e => setError(e)) 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)); .then(() => setSubmitting(false));
}; };
return ( return (

View file

@ -11,6 +11,8 @@ import CuratorInviter from './studio-curator-inviter.jsx';
import CuratorInvite from './studio-curator-invite.jsx'; import CuratorInvite from './studio-curator-invite.jsx';
import {loadCurators} from './lib/studio-member-actions'; import {loadCurators} from './lib/studio-member-actions';
import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions'; import {selectCanInviteCurators, selectShowCuratorInvite} from '../../redux/studio-permissions';
import AlertProvider from '../../components/alert/alert-provider.jsx';
import Alert from '../../components/alert/alert.jsx';
const StudioCurators = ({ const StudioCurators = ({
canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore canInviteCurators, showCuratorInvite, items, error, loading, moreToLoad, onLoadMore
@ -19,61 +21,65 @@ const StudioCurators = ({
if (items.length === 0) onLoadMore(); if (items.length === 0) onLoadMore();
}, []); }, []);
return (<div className="studio-members"> return (
<div className="studio-header-container"> <AlertProvider>
<h2><FormattedMessage id="studio.curatorsHeader" /></h2> <div className="studio-members">
</div> <Alert className="studio-alert" />
{canInviteCurators && <CuratorInviter />} <div className="studio-header-container">
{showCuratorInvite && <CuratorInvite />} <h2><FormattedMessage id="studio.curatorsHeader" /></h2>
{error && <Debug </div>
label="Error" {canInviteCurators && <CuratorInviter />}
data={error} {showCuratorInvite && <CuratorInvite />}
/>} {error && <Debug
<div className="studio-members-grid"> label="Error"
{items.length === 0 && !loading ? ( data={error}
<div className="studio-empty"> />}
<img <div className="studio-members-grid">
width="179" {items.length === 0 && !loading ? (
height="111" <div className="studio-empty">
className="studio-empty-img" <img
src="/images/studios/curators-empty.png" width="179"
/> height="111"
{canInviteCurators ? ( className="studio-empty-img"
<div className="studio-empty-msg"> src="/images/studios/curators-empty.png"
<div><FormattedMessage id="studio.curatorsEmptyCanAdd1" /></div> />
<div><FormattedMessage id="studio.curatorsEmptyCanAdd2" /></div> {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>
) : ( ) : (
<div className="studio-empty-msg"> <React.Fragment>
<div><FormattedMessage id="studio.curatorsEmpty1" /></div> {items.map(item =>
</div> (<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>
<React.Fragment> </AlertProvider>);
{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>);
}; };
StudioCurators.propTypes = { StudioCurators.propTypes = {

View file

@ -8,6 +8,8 @@ import {managers} from './lib/redux-modules';
import {loadManagers} from './lib/studio-member-actions'; import {loadManagers} from './lib/studio-member-actions';
import Debug from './debug.jsx'; import Debug from './debug.jsx';
import {ManagerTile} from './studio-member-tile.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}) => { const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
@ -16,24 +18,26 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
}, []); }, []);
return ( return (
<div className="studio-members"> <AlertProvider>
<div className="studio-header-container"> <div className="studio-members">
<h2><FormattedMessage id="studio.managersHeader" /></h2> <Alert className="studio-alert" />
</div> <div className="studio-header-container">
{error && <Debug <h2><FormattedMessage id="studio.managersHeader" /></h2>
label="Error" </div>
data={error} {error && <Debug
/>} label="Error"
<div className="studio-members-grid"> data={error}
{items.map(item => />}
(<ManagerTile <div className="studio-members-grid">
key={item.username} {items.map(item =>
id={item.id} (<ManagerTile
username={item.username} key={item.username}
image={item.profile.images['90x90']} id={item.id}
/>) username={item.username}
)} image={item.profile.images['90x90']}
{moreToLoad && />)
)}
{moreToLoad &&
<div className="studio-members-load-more"> <div className="studio-members-load-more">
<button <button
className={classNames('button', { className={classNames('button', {
@ -44,9 +48,10 @@ const StudioManagers = ({items, error, loading, moreToLoad, onLoadMore}) => {
<FormattedMessage id="general.loadMore" /> <FormattedMessage id="general.loadMore" />
</button> </button>
</div> </div>
} }
</div>
</div> </div>
</div> </AlertProvider>
); );
}; };

View file

@ -13,6 +13,7 @@ import {
removeCurator, removeCurator,
removeManager removeManager
} from './lib/studio-member-actions'; } from './lib/studio-member-actions';
import {useAlertContext} from '../../components/alert/alert-context';
import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx'; import OverflowMenu from '../../components/overflow-menu/overflow-menu.jsx';
import removeIcon from './icons/remove-icon.svg'; import removeIcon from './icons/remove-icon.svg';
@ -23,7 +24,7 @@ const StudioMemberTile = ({
username, image // own props username, image // own props
}) => { }) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null); const {errorAlert, successAlert} = useAlertContext();
const userUrl = `/users/${username}`; const userUrl = `/users/${username}`;
return ( return (
<div className="studio-member-tile"> <div className="studio-member-tile">
@ -50,11 +51,20 @@ const StudioMemberTile = ({
disabled={submitting} disabled={submitting}
onClick={() => { onClick={() => {
setSubmitting(true); setSubmitting(true);
setError(null); onPromote(username)
onPromote(username).catch(e => { .then(() => {
setError(e); successAlert({
setSubmitting(false); id: 'studio.alertManagerPromote',
}); values: {name: username}
});
})
.catch(() => {
errorAlert({
id: 'studio.alertManagerPromoteError',
values: {name: username}
});
setSubmitting(false);
});
}} }}
> >
<img src={promoteIcon} /> <img src={promoteIcon} />
@ -69,9 +79,11 @@ const StudioMemberTile = ({
disabled={submitting} disabled={submitting}
onClick={() => { onClick={() => {
setSubmitting(true); setSubmitting(true);
setError(null); onRemove(username).catch(() => {
onRemove(username).catch(e => { errorAlert({
setError(e); id: 'studio.alertMemberRemoveError',
values: {name: username}
}, null);
setSubmitting(false); setSubmitting(false);
}); });
}} }}
@ -82,7 +94,6 @@ const StudioMemberTile = ({
</li>} </li>}
</OverflowMenu> </OverflowMenu>
} }
{error && <div>{error}</div>}
</div> </div>
); );
}; };

View file

@ -8,6 +8,7 @@ import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
import {Errors, addProject} from './lib/studio-project-actions'; import {Errors, addProject} from './lib/studio-project-actions';
import UserProjectsModal from './modals/user-projects-modal.jsx'; import UserProjectsModal from './modals/user-projects-modal.jsx';
import ValidationMessage from '../../components/forms/validation-message.jsx'; import ValidationMessage from '../../components/forms/validation-message.jsx';
import {useAlertContext} from '../../components/alert/alert-context';
const errorToMessageId = error => { const errorToMessageId = error => {
switch (error) { switch (error) {
@ -26,12 +27,28 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const {successAlert} = useAlertContext();
const submit = () => { const submit = () => {
setSubmitting(true); setSubmitting(true);
setError(null); setError(null);
onSubmit(value) onSubmit(value)
.then(() => setValue('')) .then(() => {
.catch(e => setError(e)) 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)); .then(() => setSubmitting(false));
}; };
return ( return (

View file

@ -1,10 +1,11 @@
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
import React, {useState} from 'react'; import React, {useContext, useState} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import AlertContext from '../../components/alert/alert-context.js';
import {selectCanRemoveProject} from '../../redux/studio-permissions'; import {selectCanRemoveProject} from '../../redux/studio-permissions';
import {removeProject} from './lib/studio-project-actions'; import {removeProject} from './lib/studio-project-actions';
@ -16,9 +17,9 @@ const StudioProjectTile = ({
id, title, image, avatar, username // own props id, title, image, avatar, username // own props
}) => { }) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
const projectUrl = `/projects/${id}`; const projectUrl = `/projects/${id}`;
const userUrl = `/users/${username}`; const userUrl = `/users/${username}`;
const {errorAlert} = useContext(AlertContext);
return ( return (
<div className="studio-project-tile"> <div className="studio-project-tile">
<a href={projectUrl}> <a href={projectUrl}>
@ -54,11 +55,10 @@ const StudioProjectTile = ({
disabled={submitting} disabled={submitting}
onClick={() => { onClick={() => {
setSubmitting(true); setSubmitting(true);
setError(null);
onRemove(id) onRemove(id)
.catch(e => { .catch(() => {
setError(e);
setSubmitting(false); setSubmitting(false);
errorAlert({id: 'studio.alertProjectRemoveError'}, null);
}); });
}} }}
> >
@ -67,7 +67,6 @@ const StudioProjectTile = ({
</button></li> </button></li>
</OverflowMenu> </OverflowMenu>
} }
{error && <div>{error}</div>} {/* TODO where do these errors go? */}
</div> </div>
</div> </div>
); );

View file

@ -11,6 +11,8 @@ import StudioProjectAdder from './studio-project-adder.jsx';
import StudioProjectTile from './studio-project-tile.jsx'; import StudioProjectTile from './studio-project-tile.jsx';
import {loadProjects} from './lib/studio-project-actions.js'; import {loadProjects} from './lib/studio-project-actions.js';
import classNames from 'classnames'; import classNames from 'classnames';
import AlertProvider from '../../components/alert/alert-provider.jsx';
import Alert from '../../components/alert/alert.jsx';
const StudioProjects = ({ const StudioProjects = ({
canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore canAddProjects, canEditOpenToAll, items, error, loading, moreToLoad, onLoadMore
@ -20,62 +22,64 @@ const StudioProjects = ({
}, []); }, []);
return ( return (
<div className="studio-projects"> <AlertProvider>
<div className="studio-header-container"> <div className="studio-projects">
<h2><FormattedMessage id="studio.projectsHeader" /></h2> <Alert className="studio-alert" />
{canEditOpenToAll && <StudioOpenToAll />} <div className="studio-header-container">
</div> <h2><FormattedMessage id="studio.projectsHeader" /></h2>
{canAddProjects && <StudioProjectAdder />} {canEditOpenToAll && <StudioOpenToAll />}
{error && <Debug </div>
label="Error" {canAddProjects && <StudioProjectAdder />}
data={error} {error && <Debug
/>} label="Error"
<div className="studio-projects-grid"> data={error}
{items.length === 0 && !loading ? ( />}
<div className="studio-empty"> <div className="studio-projects-grid">
{canAddProjects ? ( {items.length === 0 && !loading ? (
<React.Fragment> <div className="studio-empty">
<img {canAddProjects ? (
width="388" <React.Fragment>
height="265" <img
className="studio-empty-img" width="388"
src="/images/studios/projects-empty-can-add.png" height="265"
/> className="studio-empty-img"
<div className="studio-empty-msg"> src="/images/studios/projects-empty-can-add.png"
<div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div> />
<div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div> <div className="studio-empty-msg">
</div> <div><FormattedMessage id="studio.projectsEmptyCanAdd1" /></div>
</React.Fragment> <div><FormattedMessage id="studio.projectsEmptyCanAdd2" /></div>
) : ( </div>
<React.Fragment> </React.Fragment>
<img ) : (
width="186" <React.Fragment>
height="138" <img
className="studio-empty-img" width="186"
src="/images/studios/projects-empty.png" height="138"
/> className="studio-empty-img"
<div className="studio-empty-msg"> src="/images/studios/projects-empty.png"
<div><FormattedMessage id="studio.projectsEmpty1" /></div> />
<div><FormattedMessage id="studio.projectsEmpty2" /></div> <div className="studio-empty-msg">
</div> <div><FormattedMessage id="studio.projectsEmpty1" /></div>
</React.Fragment> <div><FormattedMessage id="studio.projectsEmpty2" /></div>
)} </div>
</div> </React.Fragment>
) : ( )}
<React.Fragment> </div>
{items.map(item => ) : (
(<StudioProjectTile <React.Fragment>
fetching={loading} {items.map(item =>
key={item.id} (<StudioProjectTile
id={item.id} fetching={loading}
title={item.title} key={item.id}
image={item.image} id={item.id}
avatar={item.avatar['90x90']} title={item.title}
username={item.username} image={item.image}
addedBy={item.actor_id} avatar={item.avatar['90x90']}
/>) username={item.username}
)} addedBy={item.actor_id}
{moreToLoad && />)
)}
{moreToLoad &&
<div className="studio-projects-load-more"> <div className="studio-projects-load-more">
<button <button
className={classNames('button', { className={classNames('button', {
@ -86,11 +90,12 @@ const StudioProjects = ({
<FormattedMessage id="general.loadMore" /> <FormattedMessage id="general.loadMore" />
</button> </button>
</div> </div>
} }
</React.Fragment> </React.Fragment>
)} )}
</div>
</div> </div>
</div> </AlertProvider>
); );
}; };

View file

@ -113,7 +113,10 @@ $radius: 8px;
.active > li { background: $ui-blue; } .active > li { background: $ui-blue; }
} }
.studio-projects {} .studio-projects, .studio-members {
position: relative;
}
.studio-projects-grid { .studio-projects-grid {
margin-top: 20px; margin-top: 20px;
display: grid; display: grid;
@ -193,7 +196,6 @@ $radius: 8px;
} }
} }
.studio-members {}
.studio-members-grid { .studio-members-grid {
margin-top: 20px; margin-top: 20px;
display: grid; display: grid;