Merge pull request #2714 from LLK/hotfix/show-update-errors-hoc

[Master] Wrap project inputs to show server validation
This commit is contained in:
Ray Schamp 2019-01-25 11:49:02 -05:00 committed by GitHub
commit db99c12d89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 80 deletions

View file

@ -0,0 +1,79 @@
const PropTypes = require('prop-types');
const React = require('react');
const bindAll = require('lodash.bindall');
const api = require('../../lib/api');
const connect = require('react-redux').connect;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
class FormsyProjectUpdater extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'setRef',
'handleUpdate'
]);
this.state = {
value: props.initialValue,
error: false
};
}
componentDidUpdate () {
if (this.state.error !== false) {
const errorMessageId = this.state.error === 400 ?
'project.inappropriateUpdate' : 'general.notAvailableHeadline';
this.ref.updateInputsWithError({
[this.props.field]: this.props.intl.formatMessage({
id: errorMessageId
})
});
}
}
handleUpdate (jsonData) {
// Ignore updates that would not change the value
if (jsonData[this.props.field] === this.state.value) return;
api({
uri: `/projects/${this.props.projectInfo.id}`,
authentication: this.props.user.token,
method: 'PUT',
json: jsonData
}, (err, body, res) => {
if (res.statusCode === 200) {
this.setState({value: body[this.props.field], error: false});
} else {
this.setState({error: res.statusCode});
}
});
}
setRef (ref) {
this.ref = ref;
}
render () {
return this.props.children(
this.state.value,
this.setRef,
this.handleUpdate
);
}
}
FormsyProjectUpdater.propTypes = {
children: PropTypes.func.isRequired,
field: PropTypes.string,
initialValue: PropTypes.string,
intl: intlShape,
projectInfo: PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
}),
user: PropTypes.shape({
token: PropTypes.string
})
};
const mapStateToProps = state => ({
projectInfo: state.preview.projectInfo,
user: state.session.session.user
});
module.exports = connect(mapStateToProps)(injectIntl(FormsyProjectUpdater));

View file

@ -43,5 +43,6 @@
"project.cloudDataAlert": "This project uses cloud data - a feature that is only available to signed in Scratchers.", "project.cloudDataAlert": "This project uses cloud data - a feature that is only available to signed in Scratchers.",
"project.cloudVariables": "Cloud Variables", "project.cloudVariables": "Cloud Variables",
"project.cloudDataLink": "See Data", "project.cloudDataLink": "See Data",
"project.usernameBlockAlert": "This project can detect who is using it, through the \"username\" block. To hide your identity, sign out before using the project." "project.usernameBlockAlert": "This project can detect who is using it, through the \"username\" block. To hide your identity, sign out before using the project.",
"project.inappropriateUpdate": "Hmm...the bad word detector thinks there is a problem with your text. Please change it and remember to be respectful."
} }

View file

@ -29,6 +29,7 @@ const TopLevelComment = require('./comment/top-level-comment.jsx');
const ComposeComment = require('./comment/compose-comment.jsx'); const ComposeComment = require('./comment/compose-comment.jsx');
const ExtensionChip = require('./extension-chip.jsx'); const ExtensionChip = require('./extension-chip.jsx');
const thumbnailUrl = require('../../lib/user-thumbnail'); const thumbnailUrl = require('../../lib/user-thumbnail');
const FormsyProjectUpdater = require('./formsy-project-updater.jsx');
const projectShape = require('./projectshape.jsx').projectShape; const projectShape = require('./projectshape.jsx').projectShape;
require('./preview.scss'); require('./preview.scss');
@ -107,7 +108,6 @@ const PreviewPresentation = ({
onShare, onShare,
onToggleComments, onToggleComments,
onToggleStudio, onToggleStudio,
onUpdate,
onUpdateProjectId, onUpdateProjectId,
onUpdateProjectThumbnail, onUpdateProjectThumbnail,
originalInfo, originalInfo,
@ -233,22 +233,32 @@ const PreviewPresentation = ({
</a> </a>
<div className="title"> <div className="title">
{editable ? {editable ?
<Formsy onKeyPress={onKeyPress}> <FormsyProjectUpdater
<InplaceInput field="title"
className="project-title" initialValue={projectInfo.title}
handleUpdate={onUpdate} >
name="title" {(value, ref, handleUpdate) => (
validationErrors={{ <Formsy
maxLength: intl.formatMessage({ ref={ref}
id: 'project.titleMaxLength' onKeyPress={onKeyPress}
}) >
}} <InplaceInput
validations={{ className="project-title"
maxLength: 100 handleUpdate={handleUpdate}
}} name="title"
value={projectInfo.title} validationErrors={{
/> maxLength: intl.formatMessage({
</Formsy> : id: 'project.titleMaxLength'
})
}}
validations={{
maxLength: 100
}}
value={value}
/>
</Formsy>
)}
</FormsyProjectUpdater> :
<React.Fragment> <React.Fragment>
<div <div
className="project-title no-edit" className="project-title no-edit"
@ -377,32 +387,40 @@ const PreviewPresentation = ({
<FormattedMessage id="project.instructionsLabel" /> <FormattedMessage id="project.instructionsLabel" />
</div> </div>
{editable ? {editable ?
<Formsy <FormsyProjectUpdater
className="project-description-form" field="instructions"
onKeyPress={onKeyPress} initialValue={projectInfo.instructions}
> >
<InplaceInput {(value, ref, handleUpdate) => (
className={classNames( <Formsy
'project-description-edit', className="project-description-form"
{remixes: parentInfo && parentInfo.author} ref={ref}
)} onKeyPress={onKeyPress}
handleUpdate={onUpdate} >
name="instructions" <InplaceInput
placeholder={intl.formatMessage({ className={classNames(
id: 'project.descriptionPlaceholder' 'project-description-edit',
})} {remixes: parentInfo && parentInfo.author}
type="textarea" )}
validationErrors={{ handleUpdate={handleUpdate}
maxLength: intl.formatMessage({ name="instructions"
id: 'project.descriptionMaxLength' placeholder={intl.formatMessage({
}) id: 'project.descriptionPlaceholder'
}} })}
validations={{ type="textarea"
maxLength: 5000 validationErrors={{
}} maxLength: intl.formatMessage({
value={projectInfo.instructions} id: 'project.descriptionMaxLength'
/> })
</Formsy> : }}
validations={{
maxLength: 5000
}}
value={value}
/>
</Formsy>
)}
</FormsyProjectUpdater> :
<div className="project-description"> <div className="project-description">
{decorateText(projectInfo.instructions, { {decorateText(projectInfo.instructions, {
usernames: true, usernames: true,
@ -419,33 +437,41 @@ const PreviewPresentation = ({
<FormattedMessage id="project.notesAndCreditsLabel" /> <FormattedMessage id="project.notesAndCreditsLabel" />
</div> </div>
{editable ? {editable ?
<Formsy <FormsyProjectUpdater
className="project-description-form" field="description"
onKeyPress={onKeyPress} initialValue={projectInfo.description}
> >
<InplaceInput {(value, ref, handleUpdate) => (
className={classNames( <Formsy
'project-description-edit', className="project-description-form"
'last', ref={ref}
{remixes: parentInfo && parentInfo.author} onKeyPress={onKeyPress}
)} >
handleUpdate={onUpdate} <InplaceInput
name="description" className={classNames(
placeholder={intl.formatMessage({ 'project-description-edit',
id: 'project.notesPlaceholder' 'last',
})} {remixes: parentInfo && parentInfo.author}
type="textarea" )}
validationErrors={{ handleUpdate={handleUpdate}
maxLength: intl.formatMessage({ name="description"
id: 'project.descriptionMaxLength' placeholder={intl.formatMessage({
}) id: 'project.notesPlaceholder'
}} })}
validations={{ type="textarea"
maxLength: 5000 validationErrors={{
}} maxLength: intl.formatMessage({
value={projectInfo.description} id: 'project.descriptionMaxLength'
/> })
</Formsy> : }}
validations={{
maxLength: 5000
}}
value={value}
/>
</Formsy>
)}
</FormsyProjectUpdater> :
<div className="project-description last"> <div className="project-description last">
{decorateText(projectInfo.description, { {decorateText(projectInfo.description, {
usernames: true, usernames: true,
@ -675,7 +701,6 @@ PreviewPresentation.propTypes = {
onShare: PropTypes.func, onShare: PropTypes.func,
onToggleComments: PropTypes.func, onToggleComments: PropTypes.func,
onToggleStudio: PropTypes.func, onToggleStudio: PropTypes.func,
onUpdate: PropTypes.func,
onUpdateProjectId: PropTypes.func, onUpdateProjectId: PropTypes.func,
onUpdateProjectThumbnail: PropTypes.func, onUpdateProjectThumbnail: PropTypes.func,
originalInfo: projectShape, originalInfo: projectShape,

View file

@ -149,7 +149,7 @@ $stage-width: 480px;
$arrow-border-width: 1rem; $arrow-border-width: 1rem;
display: block; display: block;
position: absolute; position: absolute;
z-index: 1; z-index: 5;
margin-top: $arrow-border-width; margin-top: $arrow-border-width;
border: 1px solid $active-gray; border: 1px solid $active-gray;
border-radius: 5px; border-radius: 5px;

View file

@ -82,7 +82,6 @@ class Preview extends React.Component {
'handleShare', 'handleShare',
'handleUpdateProjectId', 'handleUpdateProjectId',
'handleUpdateProjectTitle', 'handleUpdateProjectTitle',
'handleUpdate',
'handleToggleComments', 'handleToggleComments',
'initCounts', 'initCounts',
'pushHistory', 'pushHistory',
@ -523,19 +522,14 @@ class Preview extends React.Component {
justShared: true justShared: true
}); });
} }
handleUpdate (jsonData) { handleUpdateProjectTitle (title) {
this.props.updateProject( this.props.updateProject(
this.props.projectInfo.id, this.props.projectInfo.id,
jsonData, {title: title},
this.props.user.username, this.props.user.username,
this.props.user.token this.props.user.token
); );
} }
handleUpdateProjectTitle (title) {
this.handleUpdate({
title: title
});
}
handleSetLanguage (locale) { handleSetLanguage (locale) {
jar.set('scratchlanguage', locale); jar.set('scratchlanguage', locale);
} }
@ -696,7 +690,6 @@ class Preview extends React.Component {
onShare={this.handleShare} onShare={this.handleShare}
onToggleComments={this.handleToggleComments} onToggleComments={this.handleToggleComments}
onToggleStudio={this.handleToggleStudio} onToggleStudio={this.handleToggleStudio}
onUpdate={this.handleUpdate}
onUpdateProjectId={this.handleUpdateProjectId} onUpdateProjectId={this.handleUpdateProjectId}
onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail} onUpdateProjectThumbnail={this.props.handleUpdateProjectThumbnail}
/> />