Merge pull request #5388 from paulkaplan/studio-project-errors

Add error display for studio curator and project adder inputs
This commit is contained in:
Paul Kaplan 2021-05-14 09:30:42 -04:00 committed by GitHub
commit e9728b8a13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 174 additions and 49 deletions

View file

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

View file

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

View file

@ -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 dont 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",

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

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