Merge pull request #2015 from benjiwheeler/report_project_endpoint

report project POSTs to scratchr2, displays modal reactively
This commit is contained in:
Benjamin Wheeler 2018-08-09 12:50:36 -04:00 committed by GitHub
commit 0114d3ea2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 260 additions and 119 deletions

View file

@ -28,8 +28,8 @@ module.exports.validationHOCFactory = defaultValidationErrors => (Component => {
<Component <Component
validationErrors={defaults( validationErrors={defaults(
{}, {},
defaultValidationErrors, props.validationErrors,
props.validationErrors defaultValidationErrors
)} )}
{...omit(props, ['validationErrors'])} {...omit(props, ['validationErrors'])}
/> />

View file

@ -35,7 +35,6 @@
.addToStudio-modal-content { .addToStudio-modal-content {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
line-height: 1.5rem;
font-size: .875rem; font-size: .875rem;
} }

View file

@ -64,11 +64,20 @@ $modal-close-size: 1rem;
.action-buttons { .action-buttons {
display: flex; display: flex;
margin: 1.125rem .8275rem .9375rem .8275rem; margin: 1.125rem .8275rem .9375rem .8275rem;
line-height: 1.5rem;
justify-content: flex-end !important; justify-content: flex-end !important;
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
/* setting overall modal to contain overflow looks good, but isn't
compatible with elements (like validation popups) that need to bleed
past modal boundary. This class can be used to force modal button
row to appear to contain overflow. */
.action-buttons-overflow-fix {
margin-bottom: .9375rem;
}
.action-button { .action-button {
margin: 0 0 0 .54625rem; margin: 0 0 0 .54625rem;
border-radius: .25rem; border-radius: .25rem;
@ -83,3 +92,19 @@ $modal-close-size: 1rem;
.action-button-text { .action-button-text {
display: flex; display: flex;
} }
.action-button.disabled {
background-color: $active-dark-gray;
}
.error-text
{
display: block;
border: 1px solid $active-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: 1rem;
min-height: 1rem;
overflow: visible;
color: $type-white;
}

View file

@ -1,10 +1,12 @@
const bindAll = require('lodash.bindall'); const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types'); const PropTypes = require('prop-types');
const React = require('react'); const React = require('react');
const connect = require('react-redux').connect;
const FormattedMessage = require('react-intl').FormattedMessage; const FormattedMessage = require('react-intl').FormattedMessage;
const injectIntl = require('react-intl').injectIntl; const injectIntl = require('react-intl').injectIntl;
const intlShape = require('react-intl').intlShape; const intlShape = require('react-intl').intlShape;
const Modal = require('../base/modal.jsx'); const Modal = require('../base/modal.jsx');
const classNames = require('classnames');
const Form = require('../../forms/form.jsx'); const Form = require('../../forms/form.jsx');
const Button = require('../../forms/button.jsx'); const Button = require('../../forms/button.jsx');
@ -12,6 +14,7 @@ const Select = require('../../forms/select.jsx');
const Spinner = require('../../spinner/spinner.jsx'); const Spinner = require('../../spinner/spinner.jsx');
const TextArea = require('../../forms/textarea.jsx'); const TextArea = require('../../forms/textarea.jsx');
const FlexRow = require('../../flex-row/flex-row.jsx'); const FlexRow = require('../../flex-row/flex-row.jsx');
const previewActions = require('../../../redux/preview.js');
require('../../forms/button.scss'); require('../../forms/button.scss');
require('./modal.scss'); require('./modal.scss');
@ -68,12 +71,24 @@ class ReportModal extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
bindAll(this, [ bindAll(this, [
'handleReportCategorySelect' 'handleCategorySelect',
'handleValid',
'handleInvalid'
]); ]);
this.state = {reportCategory: this.props.report.category}; this.state = {
category: '',
notes: '',
valid: false
};
} }
handleReportCategorySelect (name, value) { handleCategorySelect (name, value) {
this.setState({reportCategory: value}); this.setState({category: value});
}
handleValid () {
this.setState({valid: true});
}
handleInvalid () {
this.setState({valid: false});
} }
lookupPrompt (value) { lookupPrompt (value) {
const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt; const prompt = REPORT_OPTIONS.find(item => item.value === value).prompt;
@ -82,17 +97,24 @@ class ReportModal extends React.Component {
render () { render () {
const { const {
intl, intl,
isConfirmed,
isError,
isOpen,
isWaiting,
onReport, // eslint-disable-line no-unused-vars onReport, // eslint-disable-line no-unused-vars
report, onRequestClose,
type, type,
...modalProps ...modalProps
} = this.props; } = this.props;
const submitEnabled = this.state.valid && !isWaiting;
const submitDisabledParam = submitEnabled ? {} : {disabled: 'disabled'};
const contentLabel = intl.formatMessage({id: `report.${type}`}); const contentLabel = intl.formatMessage({id: `report.${type}`});
return ( return (
<Modal <Modal
className="mod-report" className="mod-report"
contentLabel={contentLabel} contentLabel={contentLabel}
isOpen={report.open} isOpen={isOpen}
onRequestClose={onRequestClose}
{...modalProps} {...modalProps}
> >
<div> <div>
@ -104,11 +126,24 @@ class ReportModal extends React.Component {
<Form <Form
className="report" className="report"
onSubmit={onReport} onInvalid={this.handleInvalid}
onValid={this.handleValid}
onValidSubmit={onReport}
> >
<div className="report-modal-content"> <div 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 <FormattedMessage
id={`report.${type}Instructions`} id={`report.${type}Instructions`}
key={`report.${type}Instructions`}
values={{ values={{
CommunityGuidelinesLink: ( CommunityGuidelinesLink: (
<a href="/community_guidelines"> <a href="/community_guidelines">
@ -117,6 +152,7 @@ class ReportModal extends React.Component {
) )
}} }}
/> />
</div>
<Select <Select
required required
elementWrapperClassName="report-modal-field" elementWrapperClassName="report-modal-field"
@ -124,10 +160,16 @@ class ReportModal extends React.Component {
name="report_category" name="report_category"
options={REPORT_OPTIONS.map(option => ({ options={REPORT_OPTIONS.map(option => ({
value: option.value, value: option.value,
label: this.props.intl.formatMessage(option.label) label: this.props.intl.formatMessage(option.label),
key: option.value
}))} }))}
value={this.state.reportCategory} validationErrors={{
onChange={this.handleReportCategorySelect} isDefaultRequiredValue: this.props.intl.formatMessage({
id: 'report.reasonMissing'
})
}}
value={this.state.category}
onChange={this.handleCategorySelect}
/> />
<TextArea <TextArea
required required
@ -135,8 +177,11 @@ class ReportModal extends React.Component {
elementWrapperClassName="report-modal-field" elementWrapperClassName="report-modal-field"
label={null} label={null}
name="notes" name="notes"
placeholder={this.lookupPrompt(this.state.reportCategory)} placeholder={this.lookupPrompt(this.state.category)}
validationErrors={{ validationErrors={{
isDefaultRequiredValue: this.props.intl.formatMessage({
id: 'report.textMissing'
}),
maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}), maxLength: this.props.intl.formatMessage({id: 'report.tooLongError'}),
minLength: this.props.intl.formatMessage({id: 'report.tooShortError'}) minLength: this.props.intl.formatMessage({id: 'report.tooShortError'})
}} }}
@ -144,28 +189,52 @@ class ReportModal extends React.Component {
maxLength: 500, maxLength: 500,
minLength: 20 minLength: 20
}} }}
value={report.notes} value={this.state.notes}
/> />
</div> </div>
)}
{isError && (
<div className="error-text">
<FormattedMessage id="report.error" />
</div>
)}
</div>
<FlexRow className="action-buttons"> <FlexRow className="action-buttons">
{report.waiting ? [ <div className="action-buttons-overflow-fix">
{isConfirmed ? (
<Button <Button
className="submit-button" className="action-button submit-button"
disabled="disabled" type="button"
key="submitButton" onClick={onRequestClose}
type="submit"
> >
<Spinner /> <div className="action-button-text">
<FormattedMessage id="general.close" />
</div>
</Button> </Button>
] : [ ) : (
<Button <Button
className="submit-button" className={classNames(
'action-button',
'submit-button',
{disabled: !submitEnabled}
)}
{...submitDisabledParam}
key="submitButton" key="submitButton"
type="submit" type="submit"
> >
{isWaiting ? (
<div className="action-button-text">
<Spinner mode="smooth" />
<FormattedMessage id="report.sending" />
</div>
) : (
<div className="action-button-text">
<FormattedMessage id="report.send" /> <FormattedMessage id="report.send" />
</div>
)}
</Button> </Button>
]} )}
</div>
</FlexRow> </FlexRow>
</Form> </Form>
</div> </div>
@ -176,15 +245,26 @@ class ReportModal extends React.Component {
ReportModal.propTypes = { ReportModal.propTypes = {
intl: intlShape, intl: intlShape,
isConfirmed: PropTypes.bool,
isError: PropTypes.bool,
isOpen: PropTypes.bool,
isWaiting: PropTypes.bool,
onReport: PropTypes.func, onReport: PropTypes.func,
onRequestClose: PropTypes.func, onRequestClose: PropTypes.func,
report: PropTypes.shape({
category: PropTypes.string,
notes: PropTypes.string,
open: PropTypes.bool,
waiting: PropTypes.bool
}),
type: PropTypes.string type: PropTypes.string
}; };
module.exports = injectIntl(ReportModal); const mapStateToProps = state => ({
isConfirmed: state.preview.status.report === previewActions.Status.FETCHED,
isError: state.preview.status.report === previewActions.Status.ERROR,
isWaiting: state.preview.status.report === previewActions.Status.FETCHING
});
const mapDispatchToProps = () => ({});
const ConnectedReportModal = connect(
mapStateToProps,
mapDispatchToProps
)(ReportModal);
module.exports = injectIntl(ConnectedReportModal);

View file

@ -9,7 +9,7 @@
margin: 100px auto; margin: 100px auto;
outline: none; outline: none;
padding: 0; padding: 0;
width: 30rem; width: 36.25rem; /* 580px; */
user-select: none; user-select: none;
} }
@ -34,26 +34,45 @@
.report-modal-content { .report-modal-content {
margin: 1rem auto; margin: 1rem auto;
width: 80%; width: 80%;
line-height: 1.5rem;
font-size: .875rem; font-size: .875rem;
.instructions {
line-height: 1.5rem;
}
.received {
margin: 0 auto;
width: 90%;
text-align: center;
line-height: 1.65rem;
.received-header {
font-weight: bold;
}
}
.error-text {
margin-top: .9375rem;
}
.validation-message { .validation-message {
$arrow-border-width: 1rem; $arrow-border-width: 1rem;
display: block; display: block;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 100%; /* position to the right of parent */
transform: translate(23.5rem, 0);
margin-left: $arrow-border-width; margin-left: $arrow-border-width;
border: 1px solid $active-gray; border: 1px solid $active-gray;
border-radius: 5px; border-radius: 5px;
background-color: $ui-orange; background-color: $ui-orange;
padding: 1rem; padding: 1rem;
min-width: 12rem;
max-width: 18.75rem; max-width: 18.75rem;
min-height: 1rem; min-height: 1rem;
overflow: visible; overflow: visible;
color: $type-white; color: $type-white;
/* arrow on box that points to the left */
&:before { &:before {
display: block; display: block;
position: absolute; position: absolute;
@ -78,3 +97,13 @@
.report-modal-field { .report-modal-field {
position: relative; position: relative;
} }
.form-group.has-error {
.textarea, select {
border: 1px solid $ui-orange;
}
}
.report-text .textarea {
margin-bottom: 0;
}

View file

@ -172,6 +172,7 @@
"registration.welcomeStepTitle": "Hurray! Welcome to Scratch!", "registration.welcomeStepTitle": "Hurray! Welcome to Scratch!",
"thumbnail.by": "by", "thumbnail.by": "by",
"report.error": "Something went wrong when trying to send your message. Please try again.",
"report.project": "Report Project", "report.project": "Report Project",
"report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.", "report.projectInstructions": "From the dropdown below, please select the reason why you feel this project is disrespectful or inappropriate or otherwise breaks the {CommunityGuidelinesLink}.",
"report.CommunityGuidelinesLinkText": "Scratch Community Guidelines", "report.CommunityGuidelinesLinkText": "Scratch Community Guidelines",
@ -181,8 +182,11 @@
"report.reasonScary": "Too Violent or Scary", "report.reasonScary": "Too Violent or Scary",
"report.reasonLanguage": "Inappropriate Language", "report.reasonLanguage": "Inappropriate Language",
"report.reasonMusic": "Inappropriate Music", "report.reasonMusic": "Inappropriate Music",
"report.reasonMissing": "Please select a reason",
"report.reasonImage": "Inappropriate Images", "report.reasonImage": "Inappropriate Images",
"report.reasonPersonal": "Sharing Personal Contact Information", "report.reasonPersonal": "Sharing Personal Contact Information",
"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.", "report.promptPlaceholder": "Select a reason why above.",
"report.promptCopy": "Please provide a link to the original project", "report.promptCopy": "Please provide a link to the original project",
"report.promptUncredited": "Please provide links to the uncredited content", "report.promptUncredited": "Please provide links to the uncredited content",
@ -194,5 +198,7 @@
"report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image", "report.promptImage": "Please say the name of the sprite or the backdrop with the inappropriate image",
"report.tooLongError": "That's too long! Please find a way to shorten your text.", "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.tooShortError": "That's too short. Please describe in detail what's inappropriate or disrespectful about the project.",
"report.send": "Send" "report.send": "Send",
"report.sending": "Sending...",
"report.textMissing": "Please tell us why you are reporting this project"
} }

View file

@ -1,3 +1,4 @@
const defaults = require('lodash.defaults');
const keyMirror = require('keymirror'); const keyMirror = require('keymirror');
const async = require('async'); const async = require('async');
const merge = require('lodash.merge'); const merge = require('lodash.merge');
@ -21,6 +22,7 @@ module.exports.getInitialState = () => ({
original: module.exports.Status.NOT_FETCHED, original: module.exports.Status.NOT_FETCHED,
parent: module.exports.Status.NOT_FETCHED, parent: module.exports.Status.NOT_FETCHED,
remixes: module.exports.Status.NOT_FETCHED, remixes: module.exports.Status.NOT_FETCHED,
report: module.exports.Status.NOT_FETCHED,
projectStudios: module.exports.Status.NOT_FETCHED, projectStudios: module.exports.Status.NOT_FETCHED,
curatedStudios: module.exports.Status.NOT_FETCHED, curatedStudios: module.exports.Status.NOT_FETCHED,
studioRequests: {} studioRequests: {}
@ -324,6 +326,7 @@ module.exports.getReplies = (projectId, commentIds) => (dispatch => {
}); });
module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => { module.exports.setFavedStatus = (faved, id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('faved', module.exports.Status.FETCHING));
if (faved) { if (faved) {
api({ api({
uri: `/projects/${id}/favorites/user/${username}`, uri: `/projects/${id}/favorites/user/${username}`,
@ -383,6 +386,7 @@ module.exports.getLovedStatus = (id, username, token) => (dispatch => {
}); });
module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => { module.exports.setLovedStatus = (loved, id, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('loved', module.exports.Status.FETCHING));
if (loved) { if (loved) {
api({ api({
uri: `/projects/${id}/loves/user/${username}`, uri: `/projects/${id}/loves/user/${username}`,
@ -531,6 +535,7 @@ module.exports.leaveStudio = (studioId, projectId, token) => (dispatch => {
}); });
module.exports.updateProject = (id, jsonData, username, token) => (dispatch => { module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
dispatch(module.exports.setFetchStatus('project', module.exports.Status.FETCHING));
api({ api({
uri: `/projects/${id}`, uri: `/projects/${id}`,
authentication: token, authentication: token,
@ -556,3 +561,27 @@ module.exports.updateProject = (id, jsonData, username, token) => (dispatch => {
dispatch(module.exports.setProjectInfo(body)); dispatch(module.exports.setProjectInfo(body));
}); });
}); });
module.exports.reportProject = (id, jsonData) => (dispatch => {
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHING));
// scratchr2 will fail if no thumbnail base64 string provided. We don't yet have
// a way to get the actual project thumbnail in www/gui, so for now just submit
// a minimal base64 png string.
defaults(jsonData, {
thumbnail: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC' +
'0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII='
});
api({
host: '',
uri: `/site-api/projects/all/${id}/report/`,
method: 'POST',
json: jsonData,
useCsrf: true
}, (err, body, res) => {
if (err || res.statusCode !== 200) {
dispatch(module.exports.setFetchStatus('report', module.exports.Status.ERROR));
return;
}
dispatch(module.exports.setFetchStatus('report', module.exports.Status.FETCHED));
});
});

View file

@ -43,7 +43,7 @@ const PreviewPresentation = ({
projectId, projectId,
projectInfo, projectInfo,
remixes, remixes,
report, reportOpen,
replies, replies,
addToStudioOpen, addToStudioOpen,
projectStudios, projectStudios,
@ -278,8 +278,8 @@ const PreviewPresentation = ({
Report Report
</Button>, </Button>,
<ReportModal <ReportModal
isOpen={reportOpen}
key="report-modal" key="report-modal"
report={report}
type="project" type="project"
onReport={onReportSubmit} onReport={onReportSubmit}
onRequestClose={onReportClose} onRequestClose={onReportClose}
@ -377,12 +377,7 @@ PreviewPresentation.propTypes = {
projectStudios: PropTypes.arrayOf(PropTypes.object), projectStudios: PropTypes.arrayOf(PropTypes.object),
remixes: PropTypes.arrayOf(PropTypes.object), remixes: PropTypes.arrayOf(PropTypes.object),
replies: PropTypes.objectOf(PropTypes.array), replies: PropTypes.objectOf(PropTypes.array),
report: PropTypes.shape({ reportOpen: PropTypes.bool,
category: PropTypes.string,
notes: PropTypes.string,
open: PropTypes.bool,
waiting: PropTypes.bool
}),
studios: PropTypes.arrayOf(PropTypes.object), studios: PropTypes.arrayOf(PropTypes.object),
userOwnsProject: PropTypes.bool userOwnsProject: PropTypes.bool
}; };

View file

@ -57,12 +57,7 @@ class Preview extends React.Component {
loveCount: 0, loveCount: 0,
projectId: parts[1] === 'editor' ? 0 : parts[1], projectId: parts[1] === 'editor' ? 0 : parts[1],
addToStudioOpen: false, addToStudioOpen: false,
report: { reportOpen: false
category: '',
notes: '',
open: false,
waiting: false
}
}; };
this.getExtensions(this.state.projectId); this.getExtensions(this.state.projectId);
this.addEventListeners(); this.addEventListeners();
@ -87,7 +82,6 @@ class Preview extends React.Component {
this.props.getRemixes(this.state.projectId); this.props.getRemixes(this.state.projectId);
this.props.getProjectStudios(this.state.projectId); this.props.getProjectStudios(this.state.projectId);
} }
} }
if (this.props.projectInfo.id !== prevProps.projectInfo.id) { if (this.props.projectInfo.id !== prevProps.projectInfo.id) {
this.getExtensions(this.state.projectId); this.getExtensions(this.state.projectId);
@ -118,7 +112,7 @@ class Preview extends React.Component {
getExtensions (projectId) { getExtensions (projectId) {
storage storage
.load(storage.AssetType.Project, projectId, storage.DataFormat.JSON) .load(storage.AssetType.Project, projectId, storage.DataFormat.JSON)
.then(projectAsset => { .then(projectAsset => { // NOTE: this is turning up null, breaking the line below.
let input = projectAsset.data; let input = projectAsset.data;
if (typeof input === 'object' && !(input instanceof ArrayBuffer) && if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
!ArrayBuffer.isView(input)) { // taken from scratch-vm !ArrayBuffer.isView(input)) { // taken from scratch-vm
@ -148,10 +142,10 @@ class Preview extends React.Component {
}); });
} }
handleReportClick () { handleReportClick () {
this.setState({report: {...this.state.report, open: true}}); this.setState({reportOpen: true});
} }
handleReportClose () { handleReportClose () {
this.setState({report: {...this.state.report, open: false}}); this.setState({reportOpen: false});
} }
handleAddToStudioClick () { handleAddToStudioClick () {
this.setState({addToStudioOpen: true}); this.setState({addToStudioOpen: true});
@ -159,27 +153,8 @@ class Preview extends React.Component {
handleAddToStudioClose () { handleAddToStudioClose () {
this.setState({addToStudioOpen: false}); this.setState({addToStudioOpen: false});
} }
// NOTE: this is a copy, change it
handleReportSubmit (formData) { handleReportSubmit (formData) {
this.setState({report: { this.props.reportProject(this.state.projectId, formData);
category: formData.report_category,
notes: formData.notes,
open: this.state.report.open,
waiting: true}
});
const data = {
...formData,
id: this.state.projectId,
user: this.props.user.username
};
console.log('submit report data', data); // eslint-disable-line no-console
this.setState({report: {
category: '',
notes: '',
open: false,
waiting: false}
});
} }
handlePopState () { handlePopState () {
const path = window.location.pathname.toLowerCase(); const path = window.location.pathname.toLowerCase();
@ -335,7 +310,7 @@ class Preview extends React.Component {
projectStudios={this.props.projectStudios} projectStudios={this.props.projectStudios}
remixes={this.props.remixes} remixes={this.props.remixes}
replies={this.props.replies} replies={this.props.replies}
report={this.state.report} reportOpen={this.state.reportOpen}
studios={this.props.studios} studios={this.props.studios}
user={this.props.user} user={this.props.user}
userOwnsProject={this.userOwnsProject()} userOwnsProject={this.userOwnsProject()}
@ -383,6 +358,7 @@ Preview.propTypes = {
projectStudios: PropTypes.arrayOf(PropTypes.object), projectStudios: PropTypes.arrayOf(PropTypes.object),
remixes: PropTypes.arrayOf(PropTypes.object), remixes: PropTypes.arrayOf(PropTypes.object),
replies: PropTypes.objectOf(PropTypes.array), replies: PropTypes.objectOf(PropTypes.array),
reportProject: PropTypes.func,
sessionStatus: PropTypes.string, sessionStatus: PropTypes.string,
setFavedStatus: PropTypes.func.isRequired, setFavedStatus: PropTypes.func.isRequired,
setFullScreen: PropTypes.func.isRequired, setFullScreen: PropTypes.func.isRequired,
@ -461,7 +437,6 @@ const mapStateToProps = state => ({
fullScreen: state.scratchGui.mode.isFullScreen fullScreen: state.scratchGui.mode.isFullScreen
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
getOriginalInfo: id => { getOriginalInfo: id => {
dispatch(previewActions.getOriginalInfo(id)); dispatch(previewActions.getOriginalInfo(id));
@ -506,6 +481,9 @@ const mapDispatchToProps = dispatch => ({
refreshSession: () => { refreshSession: () => {
dispatch(sessionActions.refreshSession()); dispatch(sessionActions.refreshSession());
}, },
reportProject: (id, formData) => {
dispatch(previewActions.reportProject(id, formData));
},
setOriginalInfo: info => { setOriginalInfo: info => {
dispatch(previewActions.setOriginalInfo(info)); dispatch(previewActions.setOriginalInfo(info));
}, },