Refactor report-modal

* addresses comments for https://github.com/LLK/scratch-www/pull/1900
* renames report fields to match scratchr2 expectations
* restyle modal to allow display of validation messages
* removed cruft
This commit is contained in:
chrisgarrity 2018-06-01 14:25:45 -04:00
parent 47b90d449b
commit 74c76fb1cf
5 changed files with 160 additions and 103 deletions

View file

@ -5,7 +5,6 @@ const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx');
const log = require('../../../lib/log.js');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
@ -20,44 +19,70 @@ class ReportModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleReasonSelect',
'handleSubmit'
'handleReportCategorySelect'
]);
this.state = {
prompt: props.intl.formatMessage({id: 'report.promptPlaceholder'}),
reason: '',
waiting: false
reportCategory: this.props.report.category,
options: [
{
value: '',
label: this.props.intl.formatMessage({id: 'report.reasonPlaceHolder'}),
prompt: this.props.intl.formatMessage({id: 'report.promptPlaceholder'})
},
{
value: '0',
label: this.props.intl.formatMessage({id: 'report.reasonCopy'}),
prompt: this.props.intl.formatMessage({id: 'report.promptCopy'})
},
{
value: '1',
label: this.props.intl.formatMessage({id: 'report.reasonUncredited'}),
prompt: this.props.intl.formatMessage({id: 'report.promptUncredited'})
},
{
value: '2',
label: this.props.intl.formatMessage({id: 'report.reasonScary'}),
prompt: this.props.intl.formatMessage({id: 'report.promptScary'})
},
{
value: '3',
label: this.props.intl.formatMessage({id: 'report.reasonLanguage'}),
prompt: this.props.intl.formatMessage({id: 'report.promptLanguage'})
},
{
value: '4',
label: this.props.intl.formatMessage({id: 'report.reasonMusic'}),
prompt: this.props.intl.formatMessage({id: 'report.promptMusic'})
},
{
value: '8',
label: this.props.intl.formatMessage({id: 'report.reasonImage'}),
prompt: this.props.intl.formatMessage({id: 'report.promptImage'})
},
{
value: '5',
label: this.props.intl.formatMessage({id: 'report.reasonPersonal'}),
prompt: this.props.intl.formatMessage({id: 'report.promptPersonal'})
},
{
value: '6',
label: this.props.intl.formatMessage({id: 'general.other'}),
prompt: this.props.intl.formatMessage({id: 'report.promptGuidelines'})
}
]
};
}
handleReasonSelect (name, value) {
const prompts = [
this.props.intl.formatMessage({id: 'report.promptCopy'}),
this.props.intl.formatMessage({id: 'report.promptUncredited'}),
this.props.intl.formatMessage({id: 'report.promptScary'}),
this.props.intl.formatMessage({id: 'report.promptLanguage'}),
this.props.intl.formatMessage({id: 'report.promptMusic'}),
this.props.intl.formatMessage({id: 'report.promptPersonal'}),
this.props.intl.formatMessage({id: 'report.promptGuidelines'}),
'not used',
this.props.intl.formatMessage({id: 'report.promptImage'})
];
this.setState({prompt: prompts[value], reason: value});
handleReportCategorySelect (name, value) {
this.setState({reportCategory: value});
}
handleSubmit (formData) {
this.setState({waiting: true});
this.props.onReport(formData, err => {
if (err) log.error(err);
this.setState({
prompt: this.props.intl.formatMessage({id: 'report.promptPlaceholder'}),
reason: '',
waiting: false
});
});
lookupPrompt (value) {
return this.state.options.find(item => item.value === value).prompt;
}
render () {
const {
intl,
onReport, // eslint-disable-line no-unused-vars
report,
type,
...modalProps
} = this.props;
@ -66,6 +91,7 @@ class ReportModal extends React.Component {
<Modal
className="mod-report"
contentLabel={contentLabel}
isOpen={report.open}
{...modalProps}
>
<div>
@ -88,68 +114,35 @@ class ReportModal extends React.Component {
/>
<Form
className="report"
onSubmit={this.handleSubmit}
onSubmit={onReport}
>
<Select
required
name="reason"
options={[
{
value: '',
label: this.props.intl.formatMessage({id: 'report.reasonPlaceHolder'})
},
{
value: '0',
label: this.props.intl.formatMessage({id: 'report.reasonCopy'})
},
{
value: '1',
label: this.props.intl.formatMessage({id: 'report.reasonUncredited'})
},
{
value: '2',
label: this.props.intl.formatMessage({id: 'report.reasonScary'})
},
{
value: '3',
label: this.props.intl.formatMessage({id: 'report.reasonLanguage'})
},
{
value: '4',
label: this.props.intl.formatMessage({id: 'report.reasonMusic'})
},
{
value: '8',
label: this.props.intl.formatMessage({id: 'report.reasonImage'})
},
{
value: '5',
label: this.props.intl.formatMessage({id: 'report.reasonPersonal'})
},
{
value: '6',
label: this.props.intl.formatMessage({id: 'general.other'})
}
]}
value={this.state.reason}
onChange={this.handleReasonSelect}
elementWrapperClassName="report-modal-field"
label={null}
name="report_category"
options={this.state.options}
value={this.state.reportCategory}
onChange={this.handleReportCategorySelect}
/>
<TextArea
required
className="report-text"
name="reportText"
placeholder={this.state.prompt}
elementWrapperClassName="report-modal-field"
label={null}
name="notes"
placeholder={this.lookupPrompt(this.state.reportCategory)}
validationErrors={{
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
}}
validations={{
// TODO find out max and min characters
maxLength: 500,
minLength: 30
minLength: 20
}}
value={report.notes}
/>
{this.state.reportWaiting ? [
{report.waiting ? [
<Button
className="submit-button white"
disabled="disabled"
@ -180,6 +173,12 @@ ReportModal.propTypes = {
intl: intlShape,
onReport: PropTypes.func,
onRequestClose: PropTypes.func,
report: PropTypes.shape({
category: PropTypes.string,
notes: PropTypes.string,
open: PropTypes.bool,
waiting: PropTypes.bool
}),
type: PropTypes.string
};

View file

@ -10,11 +10,11 @@
outline: none;
padding: 0;
width: 30rem;
overflow: hidden;
user-select: none;
}
.report-modal-header {
border-radius: 1rem 1rem 0 0;
box-shadow: inset 0 -1px 0 0 $ui-coral-dark;
background-color: $ui-coral;
padding-top: .75rem;
@ -36,4 +36,45 @@
width: 80%;
line-height: 1.5rem;
font-size: .875rem;
.validation-message {
$arrow-border-width: 1rem;
display: block;
position: absolute;
top: 0;
left: 0;
transform: translate(23.5rem, 0);
margin-left: $arrow-border-width;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
max-width: 18.75rem;
min-height: 1rem;
overflow: visible;
color: $type-white;
&:before {
display: block;
position: absolute;
top: 1rem;
left: -$arrow-border-width / 2;
transform: rotate(45deg);
border-bottom: 1px solid $active-gray;
border-left: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
width: $arrow-border-width;
height: $arrow-border-width;
content: "";
}
}
}
.report-modal-field {
position: relative;
}

View file

@ -32,7 +32,7 @@ class PreviewPresentation extends React.Component {
faved,
favoriteCount,
isFullScreen,
isReportOpen,
isLoggedIn,
isShared,
loved,
loveCount,
@ -41,6 +41,7 @@ class PreviewPresentation extends React.Component {
projectId,
projectInfo,
remixes,
report,
studios,
userOwnsProject,
onFavoriteClicked,
@ -244,7 +245,7 @@ class PreviewPresentation extends React.Component {
<Button className="action-button copy-link-button">
Copy Link
</Button>
{(!userOwnsProject) &&
{(isLoggedIn && !userOwnsProject) &&
<React.Fragment>
<Button
className="action-button report-button"
@ -254,8 +255,8 @@ class PreviewPresentation extends React.Component {
Report
</Button>,
<ReportModal
isOpen={isReportOpen}
key="report-modal"
report={report}
type="project"
onReport={onReportSubmit}
onRequestClose={onReportClose}
@ -288,7 +289,7 @@ PreviewPresentation.propTypes = {
faved: PropTypes.bool,
favoriteCount: PropTypes.number,
isFullScreen: PropTypes.bool,
isReportOpen: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isShared: PropTypes.bool,
loveCount: PropTypes.number,
loved: PropTypes.bool,
@ -304,6 +305,12 @@ PreviewPresentation.propTypes = {
projectId: PropTypes.string,
projectInfo: projectShape,
remixes: PropTypes.arrayOf(PropTypes.object),
report: PropTypes.shape({
category: PropTypes.string,
notes: PropTypes.string,
open: PropTypes.bool,
waiting: PropTypes.bool
}),
studios: PropTypes.arrayOf(PropTypes.object),
userOwnsProject: PropTypes.bool
};

View file

@ -44,7 +44,12 @@ class Preview extends React.Component {
favoriteCount: 0,
loveCount: 0,
projectId: parts[1] === 'editor' ? 0 : parts[1],
reportOpen: false
report: {
category: '',
notes: '',
open: false,
waiting: false
}
};
this.addEventListeners();
}
@ -86,19 +91,6 @@ class Preview extends React.Component {
componentWillUnmount () {
this.removeEventListeners();
}
initState () {
const pathname = window.location.pathname.toLowerCase();
const parts = pathname.split('/').filter(Boolean);
// parts[0]: 'preview'
// parts[1]: either :id or 'editor'
// parts[2]: undefined if no :id, otherwise either 'editor' or 'fullscreen'
return {
editable: false,
favoriteCount: 0,
loveCount: 0,
projectId: parts[1] === 'editor' ? 0 : parts[1]
};
}
addEventListeners () {
window.addEventListener('popstate', this.handlePopState);
}
@ -106,19 +98,31 @@ class Preview extends React.Component {
window.removeEventListener('popstate', this.handlePopState);
}
handleReportClick () {
this.setState({reportOpen: true});
this.setState({report: {...this.state.report, open: true}});
}
handleReportClose () {
this.setState({reportOpen: false});
this.setState({report: {...this.state.report, open: false}});
}
handleReportSubmit (formData) {
this.setState({report: {
category: formData.report_category,
notes: formData.notes,
open: this.state.report.open,
waiting: true}
});
const data = {
...formData,
id: this.state.projectId,
username: this.props.user.username
user: this.props.user.username
};
console.log('submit report data', data); // eslint-disable-line no-console
this.setState({reportOpen: false});
this.setState({report: {
category: '',
notes: '',
open: false,
waiting: false}
});
}
handlePopState () {
const path = window.location.pathname.toLowerCase();
@ -219,10 +223,15 @@ class Preview extends React.Component {
)
);
}
userOwnsProject () {
isLoggedIn () {
return (
this.props.sessionStatus === sessionActions.Status.FETCHED &&
Object.keys(this.props.user).length > 0 &&
Object.keys(this.props.user).length > 0
);
}
userOwnsProject () {
return (
this.isLoggedIn() &&
Object.keys(this.props.projectInfo).length > 0 &&
this.props.user.id === this.props.projectInfo.author.id
);
@ -237,7 +246,7 @@ class Preview extends React.Component {
faved={this.props.faved}
favoriteCount={this.state.favoriteCount}
isFullScreen={this.state.isFullScreen}
isReportOpen={this.state.reportOpen}
isLoggedIn={this.isLoggedIn()}
isShared={this.isShared()}
loveCount={this.state.loveCount}
loved={this.props.loved}
@ -246,6 +255,7 @@ class Preview extends React.Component {
projectId={this.state.projectId}
projectInfo={this.props.projectInfo}
remixes={this.props.remixes}
report={this.state.report}
studios={this.props.studios}
user={this.props.user}
userOwnsProject={this.userOwnsProject()}

View file

@ -237,9 +237,9 @@ $stage-height: 404px;
background-color: $ui-blue-10percent;
padding: .5rem;
width: calc(100% - (1rem + 2px));
overflow: auto;
white-space: pre-line;
font-size: 1rem;
overflow: auto;
// flex-grow
flex: 1;
}