Merge pull request #3785 from seotts/report-subcategories

[Develop] Add nested categories to project report module
This commit is contained in:
Sarah Otts 2020-04-06 13:54:39 -04:00 committed by GitHub
commit 306e1f302e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 302 additions and 87 deletions

View file

@ -14,7 +14,7 @@ const TextArea = require('../../forms/textarea.jsx');
const previewActions = require('../../../redux/preview.js');
const Progression = require('../../progression/progression.jsx');
const FormStep = require('./form-step.jsx');
const {reportOptionsShape, REPORT_OPTIONS} = require('./report-options.js');
const {reportOptionsShape, REPORT_OPTIONS} = require('./report-options.jsx');
require('../../forms/button.scss');
require('./modal.scss');
@ -23,8 +23,10 @@ require('./modal.scss');
// hard to read. Make the code easier to read by giving each step number a label.
const STEPS = {
category: 0,
textInput: 1,
confirmation: 2
subcategory: 1,
textInput: 2,
confirmation: 3,
deadend: 4
};
class ReportModal extends React.Component {
@ -32,22 +34,39 @@ class ReportModal extends React.Component {
super(props);
bindAll(this, [
'handleSetCategory',
'handleSubmit'
'handleSubmit',
'handleSetSubcategory'
]);
this.state = {
step: STEPS.category,
categoryValue: ''
categoryValue: '',
subcategoryValue: null
};
}
handleSetCategory (formData) {
const category = this.props.reportOptions.find(o => o.value === formData.category) ||
this.props.reportOptions[0];
return this.setState({
categoryValue: formData.category,
step: STEPS.textInput
step: category.subcategories ? STEPS.subcategory : STEPS.textInput
});
}
handleSetSubcategory (formData) {
const category = this.props.reportOptions.find(o => o.value === this.state.categoryValue) ||
this.props.reportOptions[0];
const subcategory = category.subcategories.find(o => o.value === formData.subcategory) ||
category.subcategories[0];
return this.setState({
subcategoryValue: subcategory.value,
step: subcategory.preventSubmission ? STEPS.deadend : STEPS.textInput
});
}
handleSubmit (formData) {
this.props.onReport({
report_category: this.state.categoryValue,
report_category: this.state.subcategoryValue ? this.state.subcategoryValue : this.state.categoryValue,
notes: formData.notes
});
}
@ -66,6 +85,13 @@ class ReportModal extends React.Component {
const contentLabel = intl.formatMessage({id: `report.${type}`});
const categoryRequiredMessage = intl.formatMessage({id: 'report.reasonMissing'});
const category = reportOptions.find(o => o.value === this.state.categoryValue) || reportOptions[0];
let finalCategory = category;
if (category.subcategories) {
finalCategory = category.subcategories.find(o => o.value === this.state.subcategoryValue) ||
category.subcategories[0];
}
// Confirmation step is shown if a report has been submitted, even if state is reset by closing the modal.
// This prevents multiple report submission within the same session because submission is stored in redux.
@ -102,7 +128,10 @@ class ReportModal extends React.Component {
key={`report.${type}Instructions`}
values={{
CommunityGuidelinesLink: (
<a href="/community_guidelines">
<a
href="/community_guidelines"
target="_blank"
>
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
</a>
)
@ -125,6 +154,33 @@ class ReportModal extends React.Component {
/>
</FormStep>
{/* Subcategory selection step */}
<FormStep
nextLabel={{id: 'general.next'}}
onNext={this.handleSetSubcategory}
>
<div className="instructions">
<div className="instructions-header">
<FormattedMessage {...category.label} />
</div>
{category.prompt}
</div>
<Select
required
elementWrapperClassName="report-modal-field"
label={null}
name="subcategory"
options={category.subcategories ? category.subcategories.map(option => ({
value: option.value,
label: intl.formatMessage(option.label),
key: option.value
})) : []}
validationErrors={{
isDefaultRequiredValue: categoryRequiredMessage
}}
/>
</FormStep>
{/* Text input step */}
<FormStep
isWaiting={isWaiting}
@ -133,9 +189,9 @@ class ReportModal extends React.Component {
>
<div className="instructions">
<div className="instructions-header">
<FormattedMessage {...category.label} />
<FormattedMessage {...finalCategory.label} />
</div>
<FormattedMessage {...category.prompt} />
{finalCategory.prompt}
</div>
<TextArea
autoFocus
@ -169,6 +225,20 @@ class ReportModal extends React.Component {
<FormattedMessage id="report.receivedBody" />
</div>
</FormStep>
{/* Deadend */}
<FormStep
submitEnabled
nextLabel={{id: 'general.close'}}
onNext={onRequestClose}
>
<div className="instructions">
<div className="instructions-header">
<FormattedMessage {...finalCategory.label} />
</div>
{finalCategory.prompt}
</div>
</FormStep>
</Progression>
</ModalInnerContent>
</div>

View file

@ -10,6 +10,8 @@
.report-modal-content {
margin: 1rem auto;
width: 80%;
font-size: .875rem;
.instructions {
line-height: 1.5rem;
@ -69,6 +71,10 @@
}
}
}
ul, p {
font-size: .875rem;
}
}
.report-modal-field {

View file

@ -1,77 +0,0 @@
const PropTypes = require('prop-types');
const {
arrayOf,
string,
shape
} = PropTypes;
/**
* Define both the PropType shape and default value for report options
* to ensure structure is validated by PropType checking going forward.
*/
const messageShape = shape({
id: string.isRequired
});
const categoryShape = shape({
value: string.isRequired,
label: messageShape.isRequired,
prompt: messageShape.isRequired
});
const reportOptionsShape = arrayOf(categoryShape);
const REPORT_OPTIONS = [
{
value: '',
label: {id: 'report.reasonPlaceHolder'},
prompt: {id: 'report.promptPlaceholder'}
},
{
value: '0',
label: {id: 'report.reasonCopy'},
prompt: {id: 'report.promptCopy'}
},
{
value: '1',
label: {id: 'report.reasonUncredited'},
prompt: {id: 'report.promptUncredited'}
},
{
value: '2',
label: {id: 'report.reasonScary'},
prompt: {id: 'report.promptScary'}
},
{
value: '3',
label: {id: 'report.reasonLanguage'},
prompt: {id: 'report.promptLanguage'}
},
{
value: '4',
label: {id: 'report.reasonMusic'},
prompt: {id: 'report.promptMusic'}
},
{
value: '8',
label: {id: 'report.reasonImage'},
prompt: {id: 'report.promptImage'}
},
{
value: '5',
label: {id: 'report.reasonPersonal'},
prompt: {id: 'report.promptPersonal'}
},
{
value: '6',
label: {id: 'general.other'},
prompt: {id: 'report.promptGuidelines'}
}
];
module.exports = {
reportOptionsShape,
REPORT_OPTIONS
};

View file

@ -0,0 +1,194 @@
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const PropTypes = require('prop-types');
const {
arrayOf,
node,
string,
shape,
bool
} = PropTypes;
/**
* Define both the PropType shape and default value for report options
* to ensure structure is validated by PropType checking going forward.
*/
const messageShape = shape({
id: string.isRequired
});
const subcategoryShape = shape({
value: string.isRequired,
label: messageShape.isRequired,
prompt: node.isRequired,
preventSubmission: bool
});
const categoryShape = shape({
value: string.isRequired,
label: messageShape.isRequired,
prompt: node.isRequired,
subcategories: arrayOf(subcategoryShape)
});
const reportOptionsShape = arrayOf(categoryShape);
const REPORT_OPTIONS = [
{
value: '',
label: {id: 'report.reasonPlaceHolder'},
prompt: <FormattedMessage id="report.promptPlaceholder" />
},
{
value: '0',
label: {id: 'report.reasonCopy'},
prompt: <FormattedMessage id="report.promptCopy" />
},
{
value: '1',
label: {id: 'report.reasonUncredited'},
prompt: <FormattedMessage id="report.promptUncredited" />
},
{
value: '2',
label: {id: 'report.reasonScary'},
prompt: <FormattedMessage id="report.promptScary" />
},
{
value: '3',
label: {id: 'report.reasonLanguage'},
prompt: <FormattedMessage id="report.promptLanguage" />
},
{
value: '4',
label: {id: 'report.reasonMusic'},
prompt: <FormattedMessage id="report.promptMusic" />
},
{
value: '8',
label: {id: 'report.reasonImage'},
prompt: <FormattedMessage id="report.promptImage" />
},
{
value: '5',
label: {id: 'report.reasonPersonal'},
prompt: <FormattedMessage id="report.promptPersonal" />
},
{
value: '6',
label: {id: 'general.other'},
prompt: <FormattedMessage id="report.promptGuidelines" />,
subcategories: [
{
value: '',
label: {id: 'report.reasonPlaceHolder'},
prompt: <FormattedMessage id="report.promptPlaceholder" />
},
{
value: 'report.reasonDontLikeIt',
label: {id: 'report.reasonDontLikeIt'},
prompt: (
<div>
<p><FormattedMessage id="report.promptDontLikeIt" /></p>
<p><FormattedMessage id="report.promptTips" /></p>
<ul>
<li><FormattedMessage id="report.tipsSupportive" /></li>
<li><FormattedMessage id="report.tipsConstructive" /></li>
<li><FormattedMessage id="report.tipsSpecific" /></li>
</ul>
</div>
),
preventSubmission: true
},
{
value: 'report.reasonDoesntWork',
label: {id: 'report.reasonDoesntWork'},
prompt: (
<div>
<p><FormattedMessage id="report.promptDoesntWork" /></p>
<p><FormattedMessage id="report.promptDoesntWorkTips" /></p>
</div>
),
preventSubmission: true
},
{
value: 'report.reasonCouldImprove',
label: {id: 'report.reasonCouldImprove'},
prompt: (
<div>
<p><FormattedMessage id="report.promptDontLikeIt" /></p>
<p><FormattedMessage id="report.promptTips" /></p>
<ul>
<li><FormattedMessage id="report.tipsSupportive" /></li>
<li><FormattedMessage id="report.tipsConstructive" /></li>
<li><FormattedMessage id="report.tipsSpecific" /></li>
</ul>
</div>
),
preventSubmission: true
},
{
value: 'report.reasonTooHard',
label: {id: 'report.reasonTooHard'},
prompt: <FormattedMessage id="report.promptTooHard" />,
preventSubmission: true
},
{
value: '9',
label: {id: 'report.reasonMisleading'},
prompt: <FormattedMessage id="report.promptMisleading" />
},
{
value: '10',
label: {id: 'report.reasonFaceReveal'},
prompt: (
<FormattedMessage
id={`report.promptFaceReveal`}
values={{
send: <FormattedMessage id="report.send" />
}}
/>
)
},
{
value: '11',
label: {id: 'report.reasonNoRemixingAllowed'},
prompt: <FormattedMessage id="report.promptNoRemixingAllowed" />
},
{
value: '12',
label: {id: 'report.reasonCreatorsSafety'},
prompt: <FormattedMessage id="report.promptCreatorsSafety" />
},
{
value: '13',
label: {id: 'report.reasonSomethingElse'},
prompt: (
<FormattedMessage
id={`report.promptSomethingElse`}
values={{
CommunityGuidelinesLink: (
<a
href="/community_guidelines"
target="_blank"
>
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
</a>
)
}}
/>
)
}
]
}
];
module.exports = {
reportOptionsShape,
REPORT_OPTIONS
};

View file

@ -258,6 +258,15 @@
"report.reasonMissing": "Please select a reason",
"report.reasonImage": "Inappropriate Images",
"report.reasonPersonal": "Sharing Personal Contact Information",
"report.reasonDontLikeIt": "I don't like this project",
"report.reasonDoesntWork": "This project does not work",
"report.reasonCouldImprove": "This project could be improved on",
"report.reasonTooHard": "This project is too hard",
"report.reasonMisleading": "The project is misleading or tricks the community",
"report.reasonFaceReveal": "It's a face reveal or is just trying to show someone's picture",
"report.reasonNoRemixingAllowed": "The project doesn't allow remixing",
"report.reasonCreatorsSafety": "I'm worried about the safety of the creator of this project",
"report.reasonSomethingElse": "Something else",
"report.receivedHeader": "We have received your report!",
"report.receivedBody": "The Scratch Team will review the project based on the Scratch community guidelines.",
"report.promptPlaceholder": "Select a reason why above.",
@ -269,6 +278,19 @@
"report.promptPersonal": "Please say where the personal contact information is shared (For example: Notes & Credits, sprite name, project text, etc.)",
"report.promptGuidelines": "Please be specific about why this project does not follow our Community Guidelines",
"report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
"report.promptDontLikeIt": "Scratch projects are made by people of all ages and levels of experience. If you don't like this project because you feel it can be improved upon, we encourage you to share constructive feedback directly with the creator.",
"report.promptTips": "Here are tips on sharing constructive feedback:",
"report.tipsSupportive": "Be supportive and encouraging.",
"report.tipsConstructive": "Leave a comment telling them what you like, but also what they could do to make the project better.",
"report.tipsSpecific": "Try to be specific with your feedback. For instance: The controls to move the character did not work.",
"report.promptDoesntWork": "A Scratch project, like any other application, may contain a few bugs. That is expected and completely okay!",
"report.promptDoesntWorkTips": "We encourage you to share any issues you discover directly with the creator of the project. It's also helpful to provide suggestions on how they may improve their project, if possible.",
"report.promptTooHard": "If you feel a project could be easier, we encourage you to share that feedback directly with the creator of the project. Or remix it yourself and make it as easy or hard as you like!",
"report.promptMisleading": "Tell us more about how it's tricking or misleading people",
"report.promptFaceReveal": "Scratch allows people to use pictures of their face in creative projects like games, stories, or animations. However, projects that are just a picture of their face, or which focus entirely on their physical appearance, aren't allowed on Scratch. If this is a \"face reveal,\" or the project focuses on the person's appearance, please click \"{send}.\"",
"report.promptNoRemixingAllowed": "Please let us know where the project says it is not okay to remix — such as in the Notes & Credits, project title, etc. ",
"report.promptCreatorsSafety": "It's important that everyone on Scratch remains safe online and in real life. Please let us know why you are worried about the safety of this user.",
"report.promptSomethingElse": "We encourage you to double check if your report fits any of the other available categories. If you strongly feel it does not, please explain why this project breaks the {CommunityGuidelinesLink}.",
"report.tooLongError": "That's too long! Please find a way to shorten your text.",
"report.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
"report.send": "Send",