mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-17 08:31:23 -05:00
Merge pull request #5388 from paulkaplan/studio-project-errors
Add error display for studio curator and project adder inputs
This commit is contained in:
commit
e9728b8a13
9 changed files with 174 additions and 49 deletions
|
@ -22,7 +22,7 @@ const ValidationMessage = props => (
|
|||
ValidationMessage.propTypes = {
|
||||
className: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
mode: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
};
|
||||
|
||||
module.exports = ValidationMessage;
|
||||
|
|
|
@ -18,7 +18,7 @@ const Errors = keyMirror({
|
|||
INAPPROPRIATE: null,
|
||||
PERMISSION: null,
|
||||
THUMBNAIL_TOO_LARGE: null,
|
||||
THUMBNAIL_MISSING: null,
|
||||
THUMBNAIL_INVALID: null,
|
||||
TEXT_TOO_LONG: null,
|
||||
REQUIRED_FIELD: null,
|
||||
UNHANDLED: null
|
||||
|
@ -111,7 +111,7 @@ const normalizeError = (err, body, res) => {
|
|||
switch (body.errors[0]) {
|
||||
case 'inappropriate-generic': return Errors.INAPPROPRIATE;
|
||||
case 'thumbnail-too-large': return Errors.THUMBNAIL_TOO_LARGE;
|
||||
case 'thumbnail-missing': return Errors.THUMBNAIL_MISSING;
|
||||
case 'image-invalid': return Errors.THUMBNAIL_INVALID;
|
||||
case 'editable-text-too-long': return Errors.TEXT_TOO_LONG;
|
||||
case 'This field is required.': return Errors.REQUIRED_FIELD;
|
||||
default: return Errors.UNHANDLED;
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
"studio.title": "Title",
|
||||
"studio.description": "Description",
|
||||
"studio.thumbnail": "Thumbnail",
|
||||
"studio.updateErrors.generic": "Something went wrong updating the studio.",
|
||||
"studio.updateErrors.inappropriate": "That seems inappropriate. Please be respectful.",
|
||||
"studio.updateErrors.textTooLong": "That is too long.",
|
||||
"studio.updateErrors.requiredField": "This cannot be blank.",
|
||||
"studio.updateErrors.thumbnailTooLarge": "Maximum file size is 512 KB and less than 500x500 pixels.",
|
||||
"studio.updateErrors.thumbnailInvalid": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
|
||||
|
||||
"studio.projectsHeader": "Projects",
|
||||
"studio.addProjectsHeader": "Add Projects",
|
||||
|
@ -20,6 +26,7 @@
|
|||
"studio.projectsEmpty1": "This studio has no projects yet.",
|
||||
"studio.projectsEmpty2": "Suggest projects you want to add in the comments!",
|
||||
"studio.browseProjects": "Browse Projects",
|
||||
"studio.projectErrors.checkUrl": "Could not add project. Check the URL and try again.",
|
||||
|
||||
"studio.creatorRole": "Studio Creator",
|
||||
|
||||
|
@ -36,6 +43,10 @@
|
|||
"studio.curatorsEmptyCanAdd1": "You don’t have curators right now.",
|
||||
"studio.curatorsEmptyCanAdd2": "Add some curators to collaborate with!",
|
||||
"studio.curatorsEmpty1": "This studio has no curators right now.",
|
||||
"studio.curatorErrors.generic": "Could not invite curator.",
|
||||
"studio.curatorErrors.alreadyCurator": "They are already part of the studio.",
|
||||
"studio.curatorErrors.unknownUsername": "Could not invite a curator with that username.",
|
||||
"studio.curatorErrors.tooFast": "You are adding curators too fast.",
|
||||
|
||||
"studio.commentsHeader": "Comments",
|
||||
"studio.comments.toggleOff": "Commenting off",
|
||||
|
|
|
@ -5,8 +5,20 @@ import {connect} from 'react-redux';
|
|||
import classNames from 'classnames';
|
||||
import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
|
||||
|
||||
import {inviteCurator} from './lib/studio-member-actions';
|
||||
import FlexRow from '../../components/flex-row/flex-row.jsx';
|
||||
import {Errors, inviteCurator} from './lib/studio-member-actions';
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
case Errors.NETWORK: return 'studio.curatorErrors.generic';
|
||||
case Errors.SERVER: return 'studio.curatorErrors.generic';
|
||||
case Errors.PERMISSION: return 'studio.curatorErrors.generic';
|
||||
case Errors.DUPLICATE: return 'studio.curatorErrors.alreadyCurator';
|
||||
case Errors.UNKNOWN_USERNAME: return 'studio.curatorErrors.unknownUsername';
|
||||
case Errors.RATE_LIMIT: return 'studio.curatorErrors.tooFast';
|
||||
default: return 'studio.curatorErrors.generic';
|
||||
}
|
||||
};
|
||||
|
||||
const StudioCuratorInviter = ({intl, onSubmit}) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
@ -23,8 +35,16 @@ const StudioCuratorInviter = ({intl, onSubmit}) => {
|
|||
return (
|
||||
<div className="studio-adder-section">
|
||||
<h3><FormattedMessage id="studio.inviteCuratorsHeader" /></h3>
|
||||
<FlexRow>
|
||||
<div className="studio-adder-row">
|
||||
{error && <div className="studio-adder-error">
|
||||
<ValidationMessage
|
||||
mode="error"
|
||||
className="validation-left"
|
||||
message={<FormattedMessage id={errorToMessageId(error)} />}
|
||||
/>
|
||||
</div>}
|
||||
<input
|
||||
className={classNames({'mod-form-error': error})}
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder={intl.formatMessage({id: 'studio.inviteCuratorPlaceholder'})}
|
||||
|
@ -39,8 +59,7 @@ const StudioCuratorInviter = ({intl, onSubmit}) => {
|
|||
disabled={submitting || value === ''}
|
||||
onClick={submit}
|
||||
><FormattedMessage id="studio.inviteCurator" /></button>
|
||||
{error && <div>{error}</div>}
|
||||
</FlexRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,23 +2,36 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectStudioDescription, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {
|
||||
mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
|
||||
Errors, mutateStudioDescription, selectIsMutatingDescription, selectDescriptionMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
case Errors.INAPPROPRIATE: return 'studio.updateErrors.inappropriate';
|
||||
case Errors.TEXT_TOO_LONG: return 'studio.updateErrors.textTooLong';
|
||||
case Errors.REQUIRED_FIELD: return 'studio.updateErrors.requiredField';
|
||||
default: return 'studio.updateErrors.generic';
|
||||
}
|
||||
};
|
||||
|
||||
const StudioDescription = ({
|
||||
descriptionError, isFetching, isMutating, description, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-description', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
'mod-mutating': isMutating,
|
||||
'mod-form-error': !!descriptionError
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="studio-info-section">
|
||||
<textarea
|
||||
rows="20"
|
||||
className={fieldClassName}
|
||||
|
@ -27,8 +40,11 @@ const StudioDescription = ({
|
|||
onBlur={e => e.target.value !== description &&
|
||||
handleUpdate(e.target.value)}
|
||||
/>
|
||||
{descriptionError && <div>Error mutating description: {descriptionError}</div>}
|
||||
</React.Fragment>
|
||||
{descriptionError && <ValidationMessage
|
||||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(descriptionError)} />}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,19 +2,30 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectStudioImage, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {
|
||||
mutateStudioImage, selectIsMutatingImage, selectImageMutationError
|
||||
Errors, mutateStudioImage, selectIsMutatingImage, selectImageMutationError
|
||||
} from '../../redux/studio-mutations';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
case Errors.THUMBNAIL_INVALID: return 'studio.updateErrors.thumbnailInvalid';
|
||||
case Errors.THUMBNAIL_TOO_LARGE: return 'studio.updateErrors.thumbnailTooLarge';
|
||||
default: return 'studio.updateErrors.generic';
|
||||
}
|
||||
};
|
||||
|
||||
const blankImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
||||
const StudioImage = ({
|
||||
imageError, isFetching, isMutating, image, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-image', {
|
||||
const fieldClassName = classNames('studio-info-section', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
});
|
||||
|
@ -36,7 +47,10 @@ const StudioImage = ({
|
|||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
{imageError && <div>Error mutating image: {imageError}</div>}
|
||||
{imageError && <ValidationMessage
|
||||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(imageError)} />}
|
||||
/>}
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import {FormattedMessage, intlShape, injectIntl} from 'react-intl';
|
|||
|
||||
import {addProject} from './lib/studio-project-actions';
|
||||
import UserProjectsModal from './modals/user-projects-modal.jsx';
|
||||
import FlexRow from '../../components/flex-row/flex-row.jsx';
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
|
||||
const StudioProjectAdder = ({intl, onSubmit}) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
@ -25,8 +25,16 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
|
|||
return (
|
||||
<div className="studio-adder-section">
|
||||
<h3><FormattedMessage id="studio.addProjectsHeader" /></h3>
|
||||
<FlexRow>
|
||||
<div className="studio-adder-row">
|
||||
{error && <div className="studio-adder-error">
|
||||
<ValidationMessage
|
||||
mode="error"
|
||||
className="validation-left"
|
||||
message={<FormattedMessage id="studio.projectErrors.checkUrl" />}
|
||||
/>
|
||||
</div>}
|
||||
<input
|
||||
className={classNames({'mod-form-error': error})}
|
||||
disabled={submitting}
|
||||
type="text"
|
||||
placeholder={intl.formatMessage({id: 'studio.addProjectPlaceholder'})}
|
||||
|
@ -41,7 +49,6 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
|
|||
disabled={submitting || value === ''}
|
||||
onClick={submit}
|
||||
><FormattedMessage id="studio.addProject" /></button>
|
||||
{error && <div>{error}</div>}
|
||||
<div className="studio-adder-vertical-divider" />
|
||||
<button
|
||||
className="button"
|
||||
|
@ -50,7 +57,7 @@ const StudioProjectAdder = ({intl, onSubmit}) => {
|
|||
<FormattedMessage id="studio.browseProjects" />
|
||||
</button>
|
||||
{modalOpen && <UserProjectsModal onRequestClose={() => setModalOpen(false)} />}
|
||||
</FlexRow>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,21 +2,38 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {selectStudioTitle, selectIsFetchingInfo} from '../../redux/studio';
|
||||
import {selectCanEditInfo} from '../../redux/studio-permissions';
|
||||
import {mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
|
||||
import classNames from 'classnames';
|
||||
import {Errors, mutateStudioTitle, selectIsMutatingTitle, selectTitleMutationError} from '../../redux/studio-mutations';
|
||||
import ValidationMessage from '../../components/forms/validation-message.jsx';
|
||||
/*
|
||||
TODO
|
||||
- no newlines in studio title
|
||||
- Correct display in read-only mode
|
||||
- validation message
|
||||
*/
|
||||
const errorToMessageId = error => {
|
||||
switch (error) {
|
||||
case Errors.INAPPROPRIATE: return 'studio.updateErrors.inappropriate';
|
||||
case Errors.TEXT_TOO_LONG: return 'studio.updateErrors.textTooLong';
|
||||
case Errors.REQUIRED_FIELD: return 'studio.updateErrors.requiredField';
|
||||
default: return 'studio.updateErrors.generic';
|
||||
}
|
||||
};
|
||||
|
||||
const StudioTitle = ({
|
||||
titleError, isFetching, isMutating, title, canEditInfo, handleUpdate
|
||||
}) => {
|
||||
const fieldClassName = classNames('studio-title', {
|
||||
'mod-fetching': isFetching,
|
||||
'mod-mutating': isMutating
|
||||
'mod-mutating': isMutating,
|
||||
'mod-form-error': !!titleError
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="studio-info-section">
|
||||
<textarea
|
||||
className={fieldClassName}
|
||||
disabled={isMutating || !canEditInfo || isFetching}
|
||||
|
@ -24,8 +41,11 @@ const StudioTitle = ({
|
|||
onBlur={e => e.target.value !== title &&
|
||||
handleUpdate(e.target.value)}
|
||||
/>
|
||||
{titleError && <div>Error mutating title: {titleError}</div>}
|
||||
</React.Fragment>
|
||||
{titleError && <ValidationMessage
|
||||
mode="error"
|
||||
message={<FormattedMessage id={errorToMessageId(titleError)} />}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -13,6 +13,12 @@ $radius: 8px;
|
|||
min-width: auto;
|
||||
margin: 50px auto;
|
||||
display: block;
|
||||
/* WAT Why does everything center at smaller widths??!! */
|
||||
@media #{$intermediate-and-smaller} {
|
||||
& {
|
||||
text-align: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-shell {
|
||||
padding: 0 20px;
|
||||
|
@ -47,14 +53,22 @@ $radius: 8px;
|
|||
border: 2px dashed $ui-blue-25percent;
|
||||
border-radius: $radius;
|
||||
resize: none;
|
||||
width: 300px;
|
||||
|
||||
&:disabled { border-color: transparent; }
|
||||
}
|
||||
|
||||
.studio-info-section {
|
||||
position: relative;
|
||||
.validation-message {
|
||||
margin-top: .5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.studio-title {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.studio-description:disabled {
|
||||
background: $ui-blue-10percent;
|
||||
}
|
||||
|
@ -220,30 +234,50 @@ $radius: 8px;
|
|||
color: #4C97FF;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
margin: 0 -6px;
|
||||
& > * {
|
||||
margin: 0 6px;
|
||||
.studio-adder-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse; /* so error goes below at small sizes */
|
||||
|
||||
.studio-adder-error {
|
||||
position: relative;
|
||||
|
||||
.validation-message {
|
||||
transform: none;
|
||||
width: 200px;
|
||||
}
|
||||
@media #{$intermediate-and-smaller} {
|
||||
& {
|
||||
width: 100%;
|
||||
margin-top: .5rem;
|
||||
.validation-message {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
flex-grow: 1;
|
||||
display: inline-block;
|
||||
margin: .5em 0;
|
||||
border: 1px solid $ui-border;
|
||||
border-radius: .5rem;
|
||||
padding: 1em 1.25em;
|
||||
font-size: .8rem;
|
||||
}
|
||||
input {
|
||||
flex-grow: 1;
|
||||
display: inline-block;
|
||||
border: 1px solid $ui-border;
|
||||
border-radius: .5rem;
|
||||
padding: 1em 1.25em;
|
||||
font-size: .8rem;
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-grow: 0;
|
||||
}
|
||||
button {
|
||||
flex-grow: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.studio-adder-vertical-divider {
|
||||
border: 1px solid $ui-border;
|
||||
align-self: stretch;
|
||||
.studio-adder-vertical-divider {
|
||||
margin: 0 6px;
|
||||
border: 1px solid $ui-border;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -328,3 +362,7 @@ $radius: 8px;
|
|||
.mod-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mod-form-error { /* When a field contains a value is causing an error */
|
||||
border-color: $ui-orange !important;
|
||||
}
|
Loading…
Reference in a new issue