Commit bc3a454f authored by Paul Kaplan's avatar Paul Kaplan

Split project report flow into multiple steps

parent a2929e5b
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const FormattedMessage = require('react-intl').FormattedMessage;
const classNames = require('classnames');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
const Spinner = require('../../spinner/spinner.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx');
require('../../forms/button.scss');
/**
* Step to be used in a form progression. Provides wrapping form element,
* renders children input elements, then provides a next button row
* that responds to form validation and submission spinner.
*/
class FormStep extends React.Component {
constructor (props) {
super(props);
this.state = {
valid: false
};
bindAll(this, [
'handleValid',
'handleInvalid'
]);
}
handleValid () {
this.setState({valid: true});
}
handleInvalid () {
this.setState({valid: false});
}
render () {
const {onNext, children, isWaiting, nextLabel} = this.props;
// Allow submit to be force-enabled by submitEnabled prop. Otherwise use form validation,
// with default being not-submittable.
const submitEnabled = (this.props.submitEnabled || this.state.valid) && !isWaiting;
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
return (
<Form
onInvalid={this.handleInvalid}
onValid={this.handleValid}
onValidSubmit={onNext}
>
{children}
<FlexRow className="action-buttons">
<Button
className={classNames(
'action-button',
'submit-button',
{disabled: !submitEnabled}
)}
{...submitDisabledParam}
key="submitButton"
type="submit"
>
{isWaiting ? (
<div className="action-button-text">
<Spinner />
<FormattedMessage id="report.sending" />
</div>
) : (
<div className="action-button-text">
<FormattedMessage {...nextLabel} />
</div>
)}
</Button>
</FlexRow>
</Form>
);
}
}
FormStep.propTypes = {
children: PropTypes.node.isRequired,
isWaiting: PropTypes.bool,
nextLabel: PropTypes.shape({id: PropTypes.string.isRequired}).isRequired,
onNext: PropTypes.func.isRequired,
submitEnabled: PropTypes.bool
};
FormStep.defaultProps = {
isWaiting: false,
submitEnabled: false
};
module.exports = FormStep;
......@@ -6,95 +6,50 @@ 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 classNames = require('classnames');
const ModalTitle = require('../base/modal-title.jsx');
const ModalInnerContent = require('../base/modal-inner-content.jsx');
const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx');
const Select = require('../../forms/select.jsx');
const Spinner = require('../../spinner/spinner.jsx');
const TextArea = require('../../forms/textarea.jsx');
const FlexRow = require('../../flex-row/flex-row.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');
require('../../forms/button.scss');
require('./modal.scss');
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'}
}
];
// Progression component only addresses steps by number, but this flow
// may skip steps so the code is easier to read with a map.
const STEPS = {
category: 0,
textInput: 1,
confirmation: 2
};
class ReportModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleCategorySelect',
'handleValid',
'handleInvalid'
'handleSetCategory',
'handleSubmit'
]);
this.state = {
category: '',
notes: '',
valid: false
step: STEPS.category,
categoryValue: ''
};
}
handleCategorySelect (name, value) {
this.setState({category: value});
}
handleValid () {
this.setState({valid: true});
handleSetCategory (formData) {
return this.setState({
categoryValue: formData.category,
step: STEPS.textInput
});
}
handleInvalid () {
this.setState({valid: false});
}
lookupPrompt (value) {
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
return this.props.intl.formatMessage(prompt);
handleSubmit (formData) {
this.props.onReport({
report_category: this.state.categoryValue,
notes: formData.notes
});
}
render () {
const {
......@@ -103,14 +58,14 @@ class ReportModal extends React.Component {
isError,
isOpen,
isWaiting,
onReport, // eslint-disable-line no-unused-vars
onRequestClose,
type,
reportOptions,
...modalProps
} = this.props;
const submitEnabled = this.state.valid && !isWaiting;
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
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];
return (
<Modal
useStandardSizes
......@@ -124,120 +79,88 @@ class ReportModal extends React.Component {
<div className="report-modal-header modal-header">
<ModalTitle title={contentLabel} />
</div>
<Form
className="report"
onInvalid={this.handleInvalid}
onValid={this.handleValid}
onValidSubmit={onReport}
>
<ModalInnerContent className="report-modal-content">
{isConfirmed ? (
<div className="received">
<div className="received-header">
<FormattedMessage id="report.receivedHeader" />
</div>
<FormattedMessage id="report.receivedBody" />
</div>
) : (
<div>
<div className="instructions">
<FormattedMessage
id={`report.${type}Instructions`}
key={`report.${type}Instructions`}
values={{
CommunityGuidelinesLink: (
<a href="/community_guidelines">
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
</a>
)
}}
/>
</div>
<Select
required
elementWrapperClassName="report-modal-field"
label={null}
name="report_category"
options={REPORT_OPTIONS.map(option => ({
value: option.value,
label: this.props.intl.formatMessage(option.label),
key: option.value
}))}
validationErrors={{
isDefaultRequiredValue: this.props.intl.formatMessage({
id: 'report.reasonMissing'
})
}}
value={this.state.category}
onChange={this.handleCategorySelect}
/>
<TextArea
required
className="report-text"
elementWrapperClassName="report-modal-field"
label={null}
name="notes"
placeholder={this.lookupPrompt(this.state.category)}
validationErrors={{
isDefaultRequiredValue: this.props.intl.formatMessage({
id: 'report.textMissing'
}),
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
}}
validations={{
maxLength: 500,
minLength: 20
<ModalInnerContent className="report-modal-content">
{isError && (
<div className="error-text">
<FormattedMessage id="report.error" />
</div>
)}
<Progression step={isConfirmed ? STEPS.confirmation : this.state.step}>
<FormStep
nextLabel={{id: 'general.next'}}
onNext={this.handleSetCategory}
>
<div className="instructions">
<FormattedMessage
id={`report.${type}Instructions`}
key={`report.${type}Instructions`}
values={{
CommunityGuidelinesLink: (
<a href="/community_guidelines">
<FormattedMessage id="report.CommunityGuidelinesLinkText" />
</a>
)
}}
value={this.state.notes}
/>
</div>
)}
{isError && (
<div className="error-text">
<FormattedMessage id="report.error" />
<Select
required
elementWrapperClassName="report-modal-field"
label={null}
name="category"
options={reportOptions.map(option => ({
value: option.value,
label: intl.formatMessage(option.label),
key: option.value
}))}
validationErrors={{
isDefaultRequiredValue: categoryRequiredMessage
}}
/>
</FormStep>
<FormStep
isWaiting={isWaiting}
nextLabel={{id: 'report.send'}}
onNext={this.handleSubmit}
>
<div className="instructions">
<div className="instructions-header">
<FormattedMessage {...category.label} />
</div>
<FormattedMessage {...category.prompt} />
</div>
)}
</ModalInnerContent>
<FlexRow className="action-buttons">
<div className="action-buttons-overflow-fix">
{isConfirmed ? (
<Button
className="action-button submit-button"
type="button"
onClick={onRequestClose}
>
<div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button>
) : (
<Button
className={classNames(
'action-button',
'submit-button',
{disabled: !submitEnabled}
)}
{...submitDisabledParam}
key="submitButton"
type="submit"
>
{isWaiting ? (
<div className="action-button-text">
<Spinner />
<FormattedMessage id="report.sending" />
</div>
) : (
<div className="action-button-text">
<FormattedMessage id="report.send" />
</div>
)}
</Button>
)}
</div>
</FlexRow>
</Form>
<TextArea
autoFocus
required
className="report-text"
elementWrapperClassName="report-modal-field"
label={null}
name="notes"
validationErrors={{
isDefaultRequiredValue: intl.formatMessage({id: 'report.textMissing'}),
maxLength: intl.formatMessage({id: 'report.tooLongError'}),
minLength: intl.formatMessage({id: 'report.tooShortError'})
}}
validations={{
maxLength: 500,
minLength: 20
}}
/>
</FormStep>
<FormStep
submitEnabled
nextLabel={{id: 'general.close'}}
onNext={onRequestClose}
>
<div className="instructions">
<div className="instructions-header">
<FormattedMessage id="report.receivedHeader" />
</div>
<FormattedMessage id="report.receivedBody" />
</div>
</FormStep>
</Progression>
</ModalInnerContent>
</div>
</Modal>
);
......@@ -252,9 +175,14 @@ ReportModal.propTypes = {
isWaiting: PropTypes.bool,
onReport: PropTypes.func,
onRequestClose: PropTypes.func,
reportOptions: reportOptionsShape,
type: PropTypes.string
};
ReportModal.defaultProps = {
reportOptions: REPORT_OPTIONS
};
const mapStateToProps = state => ({
isConfirmed: state.preview.status.report === previewActions.Status.FETCHED,
isError: state.preview.status.report === previewActions.Status.ERROR,
......
......@@ -13,15 +13,8 @@
.instructions {
line-height: 1.5rem;
}
.received {
margin: 0 auto;
width: 90%;
text-align: center;
line-height: 1.65rem;
.received-header {
.instructions-header {
font-weight: bold;
}
}
......
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
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment