diff --git a/opentech/static_src/src/app/src/components/RichTextForm/index.js b/opentech/static_src/src/app/src/components/RichTextForm/index.js index dcaad92bcb750d14b0c7fbd2f4dbf15440caaca5..7934c274885104b54ecea2bdff51752866cbf87c 100644 --- a/opentech/static_src/src/app/src/components/RichTextForm/index.js +++ b/opentech/static_src/src/app/src/components/RichTextForm/index.js @@ -11,6 +11,7 @@ export default class RichTextForm extends React.Component { disabled: PropTypes.bool.isRequired, initialValue: PropTypes.string, onValueChange: PropTypes.func, + value: PropTypes.string, }; render() { @@ -18,6 +19,7 @@ export default class RichTextForm extends React.Component { disabled: this.props.disabled, defaultValue: this.props.initialValue, onChange: this.handleValueChange, + value: this.props.value, }; return ( <textarea {...passProps} /> diff --git a/opentech/static_src/src/app/src/containers/AddNoteForm.js b/opentech/static_src/src/app/src/containers/AddNoteForm.js index 4a46b3a366515aff5bf3c67e22c21605d24703da..3a92dc695dc15fe94f319a5491d56581d80335d5 100644 --- a/opentech/static_src/src/app/src/containers/AddNoteForm.js +++ b/opentech/static_src/src/app/src/containers/AddNoteForm.js @@ -4,11 +4,17 @@ import PropTypes from 'prop-types'; import { createNoteForSubmission } from '@actions/notes'; import RichTextForm from '@components/RichTextForm'; +import { + getNoteCreatingErrorForSubmission, + getNoteCreatingStateForSubmission, +} from '@selectors/notes'; class AddNoteForm extends React.Component { static propTypes = { submitNote: PropTypes.func, submissionID: PropTypes.number, + error: PropTypes.any, + isCreating: PropTypes.bool, }; state = { @@ -16,30 +22,50 @@ class AddNoteForm extends React.Component { }; render() { + const { error, isCreating } = this.props; return ( <> - <RichTextForm onValueChange={this.setText} /> - <button onClick={this.onSubmit}>Submit</button> + {Boolean(error) && <p>{error}</p>} + <RichTextForm + disabled={isCreating} + value={this.state.text} + onValueChange={this.setText} /> + <button + disabled={!this.state.text.trim() || isCreating} + onClick={this.onSubmit} + > + Submit + </button> </> ); } - onSubmit = () => { - this.props.submitNote(this.props.submissionID, { - message: this.state.text, + onSubmit = async () => { + const action = await this.props.submitNote(this.props.submissionID, { + message: this.state.text.trim(), visibility: 'internal', }); + if (action === true) { + this.setState({ + text: '' + }); + } } setText = text => { this.setState({ - text: text.trim(), + text }); } } +const mapStateToProps = (state, ownProps) => ({ + error: getNoteCreatingErrorForSubmission(ownProps.submissionID)(state), + isCreating: getNoteCreatingStateForSubmission(ownProps.submissionID)(state), +}); + const mapDispatchToProps = dispatch => ({ submitNote: (submissionID, note) => dispatch(createNoteForSubmission(submissionID, note)), }); -export default connect(null, mapDispatchToProps)(AddNoteForm); +export default connect(mapStateToProps, mapDispatchToProps)(AddNoteForm); diff --git a/opentech/static_src/src/app/src/redux/actions/notes.js b/opentech/static_src/src/app/src/redux/actions/notes.js index d7ebe1e657c139ec24a5d9812a9847ba02067ec1..49b3bacde145424d05dfbedaacac0b5aef7f1380 100644 --- a/opentech/static_src/src/app/src/redux/actions/notes.js +++ b/opentech/static_src/src/app/src/redux/actions/notes.js @@ -1,4 +1,4 @@ -import { updateSubmission } from '@actions/submissions'; +import { updateSubmission, appendNoteIDForSubmission } from '@actions/submissions'; import api from '@api'; export const FAIL_FETCHING_NOTES = 'FAIL_FETCHING_NOTES'; @@ -6,6 +6,9 @@ export const START_FETCHING_NOTES = 'START_FETCHING_NOTES'; export const UPDATE_NOTES = 'UPDATE_NOTES'; export const UPDATE_NOTE = 'UPDATE_NOTE'; +export const START_CREATING_NOTE_FOR_SUBMISSION = 'START_CREATING_NOTE_FOR_SUBMISSION'; +export const FAIL_CREATING_NOTE_FOR_SUBMISSION = 'FAIL_CREATING_NOTE_FOR_SUBMISSION'; + const startFetchingNotes = () => ({ type: START_FETCHING_NOTES, }); @@ -45,20 +48,47 @@ export const updateNotes = data => ({ data, }); +const startCreatingNoteForSubmission = submissionID => ({ + type: START_CREATING_NOTE_FOR_SUBMISSION, + submissionID +}); + +const failCreatingNoteForSubmission = (submissionID, error) => ({ + type: FAIL_CREATING_NOTE_FOR_SUBMISSION, + submissionID, + error +}); + +const updateNote = (data, submissionID) => ({ + type: UPDATE_NOTE, + submissionID, + data, +}); + +const createdNoteForSubmission = (submissionID, data) => { + return function(dispatch) { + dispatch(updateNote(data, submissionID)); + dispatch(appendNoteIDForSubmission(submissionID, data.id)); + return true; + }; +}; export const createNoteForSubmission = (submissionID, note) => { return async function(dispatch) { + dispatch(startCreatingNoteForSubmission(submissionID)); try { const response = await api.createNoteForSubmission(submissionID, note); const json = await response.json(); if (!response.ok) { - console.error(json); - return; + return dispatch(failCreatingNoteForSubmission( + submissionID, + JSON.stringify(json) + )); } - return; + return dispatch(createdNoteForSubmission(submissionID, json)); } catch (e) { console.error(e); - return; + return dispatch(failCreatingNoteForSubmission(submissionID, e.message)); } } -} +}; diff --git a/opentech/static_src/src/app/src/redux/actions/submissions.js b/opentech/static_src/src/app/src/redux/actions/submissions.js index 181f755f4fb588a3338a82c948b5ecd6a9b9a8ec..96014f0c50bfe0f40ffa2b2a7ccd15d037945393 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -20,6 +20,9 @@ export const FAIL_LOADING_SUBMISSION = 'FAIL_LOADING_SUBMISSION'; export const UPDATE_SUBMISSION = 'UPDATE_SUBMISSION'; export const CLEAR_CURRENT_SUBMISSION = 'CLEAR_CURRENT_SUBMISSION'; +// Notes +export const APPEND_NOTE_ID_FOR_SUBMISSION = 'APPEND_NOTE_ID_FOR_SUBMISSION'; + export const setCurrentSubmissionRound = id => ({ type: SET_CURRENT_SUBMISSION_ROUND, id, @@ -132,3 +135,9 @@ export const updateSubmission = (submissionID, data) => ({ export const clearCurrentSubmission = () => ({ type: CLEAR_CURRENT_SUBMISSION, }); + +export const appendNoteIDForSubmission = (submissionID, noteID) => ({ + type: APPEND_NOTE_ID_FOR_SUBMISSION, + submissionID, + noteID, +}); diff --git a/opentech/static_src/src/app/src/redux/reducers/notes.js b/opentech/static_src/src/app/src/redux/reducers/notes.js index 4137c6103999494d66ad54784cbb75397b974a0e..eeca41483008eb448e9dd860d483a0af6a3b4a9f 100644 --- a/opentech/static_src/src/app/src/redux/reducers/notes.js +++ b/opentech/static_src/src/app/src/redux/reducers/notes.js @@ -5,6 +5,8 @@ import { UPDATE_NOTES, START_FETCHING_NOTES, FAIL_FETCHING_NOTES, + START_CREATING_NOTE_FOR_SUBMISSION, + FAIL_CREATING_NOTE_FOR_SUBMISSION, } from '@actions/notes'; function notesFetching(state = false, action) { @@ -43,8 +45,44 @@ function note(state, action) { } } +function notesCreating(state = [], action) { + switch (action.type) { + case START_CREATING_NOTE_FOR_SUBMISSION: + return [ + ...state, + action.submissionID, + ]; + case UPDATE_NOTE: + case FAIL_CREATING_NOTE_FOR_SUBMISSION: + return state.filter(v => v !== action.submissionID); + default: + return state + } +} + + +function notesFailedCreating(state = {}, action) { + switch (action.type) { + case UPDATE_NOTE: + case START_CREATING_NOTE_FOR_SUBMISSION: + return Object.entries(state).reduce((acc, [k, v]) => { + if (k !== action.submissionID) { + acc[k] = v; + } + return acc; + }, {}); + case FAIL_CREATING_NOTE_FOR_SUBMISSION: + return { + ...state, + [action.submissionID]: action.error, + }; + default: + return state + } +} + function notesByID(state = {}, action) { - switch(action.type) { + switch (action.type) { case UPDATE_NOTES: return { ...state, @@ -56,6 +94,14 @@ function notesByID(state = {}, action) { return newNotesAccumulator; }, {}), }; + case UPDATE_NOTE: + return { + ...state, + [action.data.id]: note(state[action.data.id], { + type: UPDATE_NOTE, + data: action.data, + }), + }; default: return state; } @@ -65,4 +111,6 @@ export default combineReducers({ byID: notesByID, isFetching: notesFetching, isErrored: notesErrored, + createError: notesFailedCreating, + isCreating: notesCreating, }); diff --git a/opentech/static_src/src/app/src/redux/reducers/submissions.js b/opentech/static_src/src/app/src/redux/reducers/submissions.js index 7563d1e25f0ac19364f82d0c96af3624f2125d4c..d77288614357140ace384fad7de9cb3aa03b8240 100644 --- a/opentech/static_src/src/app/src/redux/reducers/submissions.js +++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js @@ -7,6 +7,7 @@ import { UPDATE_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSION, SET_CURRENT_SUBMISSION, + APPEND_NOTE_ID_FOR_SUBMISSION, } from '@actions/submissions'; @@ -31,6 +32,14 @@ function submission(state, action) { isFetching: false, isErrored: false, }; + case APPEND_NOTE_ID_FOR_SUBMISSION: + return { + ...state, + comments: [ + action.noteID, + ...state.comments + ] + }; default: return state; } @@ -41,6 +50,7 @@ function submissionsByID(state = {}, action) { switch(action.type) { case START_LOADING_SUBMISSION: case FAIL_LOADING_SUBMISSION: + case APPEND_NOTE_ID_FOR_SUBMISSION: case UPDATE_SUBMISSION: return { ...state, diff --git a/opentech/static_src/src/app/src/redux/selectors/notes.js b/opentech/static_src/src/app/src/redux/selectors/notes.js index cf5ccc2fd82a54c81ba08884242409ac6b565117..cd035999ff96834f3262b618a24f43750aa251b8 100644 --- a/opentech/static_src/src/app/src/redux/selectors/notes.js +++ b/opentech/static_src/src/app/src/redux/selectors/notes.js @@ -16,3 +16,15 @@ export const getNoteIDsForSubmissionOfID = submissionID => createSelector( [getSubmissionOfID(submissionID)], submission => (submission || {}).comments || [] ); + +const getNoteCreatingErrors = state => state.notes.createError; + +export const getNoteCreatingErrorForSubmission = submissionID => createSelector( + [getNoteCreatingErrors], errors => errors[submissionID] +); + +const getNoteCreatingState = state => state.notes.isCreating; + +export const getNoteCreatingStateForSubmission = submissionID => createSelector( + [getNoteCreatingState], creatingStates => creatingStates.includes(submissionID) +);