diff --git a/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss b/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss index 621045e7ffa8451da1b8c899354d635d38f7c416..e1f565e70f112a3002153473c8deef6473c85909 100644 --- a/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss +++ b/opentech/static_src/src/app/src/components/LoadingPanel/styles.scss @@ -1,4 +1,5 @@ .loading-panel { + height: 100%; text-align: center; padding: 20px; diff --git a/opentech/static_src/src/app/src/components/NoteListingItem/index.js b/opentech/static_src/src/app/src/components/NoteListingItem/index.js index d9d3da3b872c4fd53b2627650a0f97cb45778c40..ef67af8e6e119ea857977adc32b5970b7b482c2f 100644 --- a/opentech/static_src/src/app/src/components/NoteListingItem/index.js +++ b/opentech/static_src/src/app/src/components/NoteListingItem/index.js @@ -5,7 +5,7 @@ import './styles.scss'; export default class NoteListingItem extends React.Component { static propTypes = { - user: PropTypes.string.isRequired, + author: PropTypes.string.isRequired, message: PropTypes.string.isRequired, timestamp: PropTypes.string.isRequired, handleEditNote: PropTypes.func.isRequired, @@ -14,18 +14,19 @@ export default class NoteListingItem extends React.Component { }; parseUser() { - const { user } = this.props; + const { author } = this.props; - if (user.length > 16) { - return `${user.substring(0, 16)}...` + if (author.length > 16) { + return `${author.substring(0, 16)}...` } else { - return user; + return author; } } render() { const { timestamp, message, handleEditNote, disabled, editable} = this.props; + return ( <li className={`note ${disabled ? 'disabled' : ''}`}> <p className="note__meta"> 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 140ab8bc2d8f12568accb862c1d31671c3bd8095..30633c22758dbfaeee418ce0bc45883126fcb473 100644 --- a/opentech/static_src/src/app/src/components/RichTextForm/index.js +++ b/opentech/static_src/src/app/src/components/RichTextForm/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState, useEffect} from 'react'; import PropTypes from 'prop-types'; import RichTextEditor from 'react-rte'; @@ -22,92 +22,89 @@ const toolbarConfig = { ] }; -export default class RichTextForm extends React.Component { - static defaultProps = { - disabled: false, - initialValue: '', - }; - - static propTypes = { - disabled: PropTypes.bool.isRequired, - onValueChange: PropTypes.func, - value: PropTypes.string, - instance: PropTypes.string, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - onCancel: PropTypes.func, - initialValue: PropTypes.string, - }; - - state = { - value: RichTextEditor.createEmptyValue(), - emptyState: RichTextEditor.createEmptyValue().toString('html'), - }; - - componentDidMount() { - const {initialValue} = this.props - - if (initialValue) { - this.setState({ value: RichTextEditor.createValueFromString(initialValue, 'html') }); - } - } - resetEditor = () => { - this.setState({value: RichTextEditor.createEmptyValue()}); - } +const emptyState = RichTextEditor.createEmptyValue().toString('html') - render() { - const { instance, disabled } = this.props; - - return ( - <div className={ instance } > - <RichTextEditor - disabled={ disabled } - onChange={ this.handleValueChange } - value={ this.state.value } - className="add-note-form__container" - toolbarClassName="add-note-form__toolbar" - editorClassName="add-note-form__editor" - toolbarConfig={toolbarConfig} - /> - <div> - <button - disabled={this.isEmpty() || disabled} - onClick={this.handleSubmit} - className={`button ${instance}__button`} - > - Submit - </button> - <button - disabled={this.isEmpty() || disabled} - onClick={this.handleCancel} - className={`button ${instance}__button`} - > - Cancel - </button> - </div> - </div> - ); + +const RichTextForm = ({initialValue, onChange, onCancel, onSubmit, instance, disabled}) => { + const [value, updateValue] = useState(RichTextEditor.createValueFromString(initialValue, 'html')) + + useEffect(() => { + updateValue(RichTextEditor.createValueFromString(initialValue, 'html')) + }, [initialValue]) + + const resetEditor = () => { + updateValue(RichTextEditor.createEmptyValue()) } - isEmpty = () => { - return !this.state.value.getEditorState().getCurrentContent().hasText(); + const isEmpty = () => { + return !value.getEditorState().getCurrentContent().hasText(); } - handleValueChange = (value) => { - const html = value.toString('html') - if (html !== this.state.emptyState ) { - this.props.onChange && this.props.onChange(html) - this.setState({value}); + const handleValueChange = (newValue) => { + const html = newValue.toString('html') + if ( html !== emptyState || value.toString('html') !== emptyState ) { + onChange && onChange(html) } + updateValue(newValue) } - handleCancel = () => { - this.props.onCancel(); - this.resetEditor() + const handleCancel = () => { + onCancel(); + resetEditor() } - handleSubmit = () => { - this.props.onSubmit(this.state.value.toString('html'), this.resetEditor); + const handleSubmit = () => { + onSubmit(value.toString('html'), resetEditor); } + + return ( + <div className={ instance } > + <RichTextEditor + disabled={ disabled } + onChange={ handleValueChange } + value={ value } + className="add-note-form__container" + toolbarClassName="add-note-form__toolbar" + editorClassName="add-note-form__editor" + toolbarConfig={toolbarConfig} + /> + <div> + <button + disabled={isEmpty() || disabled} + onClick={handleSubmit} + className={`button ${instance}__button`} + > + Submit + </button> + <button + disabled={disabled} + onClick={handleCancel} + className={`button ${instance}__button`} + > + Cancel + </button> + </div> + </div> + ); + } + +RichTextForm.defaultProps = { + disabled: false, + initialValue: '', +}; + +RichTextForm.propTypes = { + disabled: PropTypes.bool.isRequired, + onValueChange: PropTypes.func, + value: PropTypes.string, + instance: PropTypes.string, + onSubmit: PropTypes.func, + onChange: PropTypes.func, + onCancel: PropTypes.func, + initialValue: PropTypes.string, +}; + + +export default RichTextForm; diff --git a/opentech/static_src/src/app/src/containers/AddNoteForm.js b/opentech/static_src/src/app/src/containers/AddNoteForm.js index 74d3503b991414c9c71f77efb42b5d964ad6c3b7..45fb64304fd630e32dd014ed10aa0eeb60f2b0ee 100644 --- a/opentech/static_src/src/app/src/containers/AddNoteForm.js +++ b/opentech/static_src/src/app/src/containers/AddNoteForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; @@ -19,6 +19,12 @@ import './AddNoteForm.scss'; const AddNoteForm = ({error, isCreating, draftNote, submitNote, storeNote, clearNote, submissionID}) => { + const [initialValue, updateInitialValue] = useState() + + useEffect(() => { + updateInitialValue(draftNote && draftNote.message || '') + }, [submissionID]) + const onSubmit = (message, resetEditor) => { submitNote(submissionID, { message, @@ -30,7 +36,7 @@ const AddNoteForm = ({error, isCreating, draftNote, submitNote, storeNote, clear <> {Boolean(error) && <p>{error}</p>} <RichTextForm - initialValue={draftNote && draftNote.message} + initialValue={initialValue} disabled={isCreating} onCancel={() => clearNote(submissionID)} onChange={(message) => storeNote(submissionID, message)} diff --git a/opentech/static_src/src/app/src/containers/EditNoteForm.js b/opentech/static_src/src/app/src/containers/EditNoteForm.js index b211445489921c0be1f9789771f711eba38a8db2..7b65551c87849f3bfc817636a8cb264214624d6e 100644 --- a/opentech/static_src/src/app/src/containers/EditNoteForm.js +++ b/opentech/static_src/src/app/src/containers/EditNoteForm.js @@ -2,21 +2,22 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { editNoteForSubmission, handleRemoveNote } from '@actions/notes'; -import { removeNoteFromSubmission } from '@actions/submissions'; -import { getDraftNoteForSubmission } from '@selectors/notes'; -import RichTextForm from '@components/RichTextForm'; - import { + editNoteForSubmission, + removedStoredNote, + writingNote, +} from '@actions/notes'; +import { + getDraftNoteForSubmission, getNoteCreatingErrorForSubmission, getNoteCreatingStateForSubmission, } from '@selectors/notes'; +import RichTextForm from '@components/RichTextForm'; import './AddNoteForm.scss'; class EditNoteForm extends React.Component { static propTypes = { - submitNote: PropTypes.func, submissionID: PropTypes.number, error: PropTypes.any, isCreating: PropTypes.bool, @@ -25,13 +26,13 @@ class EditNoteForm extends React.Component { timestamp: PropTypes.string, message: PropTypes.string, }), - updateNotes: PropTypes.func, - removeEditedNote: PropTypes.func, - removeNoteFromSubmission: PropTypes.func, + submitNote: PropTypes.func, + storeNote: PropTypes.func, + clearNote: PropTypes.func, }; render() { - const { error, isCreating, draftNote} = this.props; + const { error, isCreating, draftNote, clearNote, submissionID} = this.props; return ( <> @@ -39,7 +40,7 @@ class EditNoteForm extends React.Component { <RichTextForm disabled={isCreating} onSubmit={this.onSubmit} - onCancel={this.clearEditingNote} + onCancel={() => clearNote(submissionID)} instance="add-note-form" initialValue={draftNote.message} /> @@ -47,18 +48,11 @@ class EditNoteForm extends React.Component { ); } - clearEditingNote = () => { - this.props.removeEditedNote(this.props.submissionID); - } - onSubmit = (message, resetEditor) => { this.props.submitNote({ ...this.props.draftNote, message, - }).then(() => { - this.props.removeNoteFromSubmission(this.props.submissionID, this.props.draftNote); - this.clearEditingNote() - }); + }, this.props.submissionID); } } @@ -69,9 +63,9 @@ const mapStateToProps = (state, ownProps) => ({ }); const mapDispatchToProps = (dispatch, ownProps) => ({ - submitNote: (note) => dispatch(editNoteForSubmission(note)), - removeEditedNote: (submissionID, note) => dispatch(handleRemoveNote(submissionID, note)), - removeNoteFromSubmission: (submissionID, note) => dispatch(removeNoteFromSubmission(submissionID, note)), + submitNote: (note, submissionID) => dispatch(editNoteForSubmission(note, submissionID)), + storeNote: (submissionID, message) => dispatch(writingNote(submissionID, message)), + clearNote: (submissionID) => dispatch(removedStoredNote(submissionID)), }); export default connect(mapStateToProps, mapDispatchToProps)(EditNoteForm); diff --git a/opentech/static_src/src/app/src/containers/Note.js b/opentech/static_src/src/app/src/containers/Note.js deleted file mode 100644 index 8399ca15d275b6642b4a06b8e05374af17d90c2d..0000000000000000000000000000000000000000 --- a/opentech/static_src/src/app/src/containers/Note.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; - -import { getNoteOfID } from '@selectors/notes'; -import NoteListingItem from '@components/NoteListingItem'; -import { handleEditNote } from '@actions/notes'; - -class Note extends React.Component { - static propTypes = { - handleEditNote: PropTypes.func, - submissionID: PropTypes.number, - disabled: PropTypes.bool, - note: PropTypes.shape({ - user: PropTypes.string, - timestamp: PropTypes.string, - message: PropTypes.string, - }), - }; - - render() { - const { note, handleEditNote, disabled } = this.props; - - const date = new Date(note.timestamp).toLocaleDateString('en-gb', {day: 'numeric', month: 'short', year:'numeric', timezone:'GMT'}) - - return <NoteListingItem - disabled={disabled} - user={note.user} - message={note.message} - timestamp={date} - handleEditNote={() => handleEditNote(note.message)} - editable={note.editable} - />; - } -} - -const mapStateToProps = (state, ownProps) => ({ - note: getNoteOfID(ownProps.noteID)(state), -}); - -const mapDispatchToProps = (dispatch, ownProps) => ({ - handleEditNote: (message) => dispatch(handleEditNote(ownProps.noteID, ownProps.submissionID, message)), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(Note); diff --git a/opentech/static_src/src/app/src/containers/NoteListing.js b/opentech/static_src/src/app/src/containers/NoteListing.js index 1f42284a719fb8aff4dc8455eca84be07ec670d8..d522691c4a3f727e838365fdd36b20bbf4db26cb 100644 --- a/opentech/static_src/src/app/src/containers/NoteListing.js +++ b/opentech/static_src/src/app/src/containers/NoteListing.js @@ -4,9 +4,9 @@ import { connect } from 'react-redux'; import useInterval from "@rooks/use-interval" -import { fetchNewNotesForSubmission } from '@actions/notes'; +import { fetchNewNotesForSubmission, editingNote } from '@actions/notes'; import Listing from '@components/Listing'; -import Note from '@containers/Note'; +import NoteListingItem from '@components/NoteListingItem'; import { getNotesErrorState, getNotesErrorMessage, @@ -16,7 +16,7 @@ import { } from '@selectors/notes'; -const NoteListing = ({ loadNotes, submissionID, notes, isErrored, errorMessage, isLoading, editing }) => { +const NoteListing = ({ loadNotes, submissionID, notes, isErrored, errorMessage, isLoading, editing, editNote }) => { const fetchNotes = () => loadNotes(submissionID) const {start, stop } = useInterval(fetchNotes, 30000) @@ -39,7 +39,20 @@ const NoteListing = ({ loadNotes, submissionID, notes, isErrored, errorMessage, const orderedNotes = notes.sort((a,b) => a.timestamp - b.timestamp); - const renderItem = note => <Note key={`note-${note.id}`} noteID={note.id} submissionID={submissionID} disabled={!!editing} />; + const renderItem = note => { + const date = new Date(note.timestamp).toLocaleDateString('en-gb', {day: 'numeric', month: 'short', year:'numeric', timezone:'GMT'}) + + return <NoteListingItem + author={note.user} + timestamp={date} + key={`note-${note.id}`} + message={note.message} + submissionID={submissionID} + disabled={!!editing} + editable={note.editable} + handleEditNote={() => editNote(note.id, note.message, submissionID)} + />; + } return ( <Listing @@ -56,6 +69,7 @@ const NoteListing = ({ loadNotes, submissionID, notes, isErrored, errorMessage, NoteListing.propTypes = { loadNotes: PropTypes.func, + editNote: PropTypes.func, submissionID: PropTypes.number, notes: PropTypes.array, isErrored: PropTypes.bool, @@ -67,6 +81,7 @@ NoteListing.propTypes = { const mapDispatchToProps = dispatch => ({ loadNotes: submissionID => dispatch(fetchNewNotesForSubmission(submissionID)), + editNote: (id, message, submissionID) => dispatch(editingNote(id, message, submissionID)), }); const mapStateToProps = (state, ownProps) => ({ @@ -75,6 +90,7 @@ const mapStateToProps = (state, ownProps) => ({ isErrored: getNotesErrorState(state), errorMessage: getNotesErrorMessage(state), editing: getDraftNoteForSubmission(ownProps.submissionID)(state), + }); export default connect(mapStateToProps, mapDispatchToProps)(NoteListing); 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 9f0e5164d510f7af13fa23c6f00004332a0cff2e..6b233e39198e82a0f787d486f5481dacca74a529 100644 --- a/opentech/static_src/src/app/src/redux/actions/notes.js +++ b/opentech/static_src/src/app/src/redux/actions/notes.js @@ -62,7 +62,7 @@ const fetchNewerNotes = (submissionID, latestID) => ({ }) -export const editingNote = (submissionID, messageID, message) => ({ +export const editingNote = (messageID, message, submissionID) => ({ type: STORE_NOTE, messageID, submissionID, @@ -78,14 +78,15 @@ export const writingNote = (submissionID, message) => ({ }) -export const editNoteForSubmission = (note) => (dispatch) => dispatch(editNote(note)) +export const editNoteForSubmission = (note, submissionID) => (dispatch) => dispatch(editNote(note, submissionID)) -const editNote = (note) => ({ +const editNote = (note, submissionID) => ({ [CALL_API]: { types: [ START_EDITING_NOTE_FOR_SUBMISSION, UPDATE_NOTE, FAIL_EDITING_NOTE_FOR_SUBMISSION ], endpoint: api.editNoteForSubmission(note), }, - note + note, + submissionID, }) 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 fb9bfad1f38dfc6e0b97ed365137e58f3944569d..ac007febd92675316470069cd7ce34aaec2d5bc0 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -25,11 +25,6 @@ import { addMessage, } from '@actions/messages'; -import { - fetchNewNotesForSubmission -} from '@actions/notes'; - - // Round export const UPDATE_ROUND = 'UPDATE_ROUND'; export const START_LOADING_ROUND = 'START_LOADING_ROUND'; @@ -66,7 +61,6 @@ export const FAIL_EXECUTING_SUBMISSION_ACTION = 'FAIL_EXECUTING_SUBMISSION_ACTIO // Notes export const ADD_NOTE_FOR_SUBMISSION = 'ADD_NOTE_FOR_SUBMISSION'; -export const REMOVE_NOTE_FROM_SUBMISSION = 'REMOVE_NOTE_FROM_SUBMISSION' export const setCurrentSubmissionRound = (id) => (dispatch) => { dispatch({ @@ -294,12 +288,3 @@ export const executeSubmissionAction = (submissionID, action) => ({ submissionID, changedLocally: true, }) - -export const removeNoteFromSubmission = (submissionID, note) => (dispatch) => { - dispatch ({ - type: REMOVE_NOTE_FROM_SUBMISSION, - submissionID, - note - }) - return dispatch(fetchNewNotesForSubmission(submissionID)) -} 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 a86a79b341c8961ec2e310346d73e025cd189050..1bed582cde626bb63e81470efeae65761601813d 100644 --- a/opentech/static_src/src/app/src/redux/reducers/notes.js +++ b/opentech/static_src/src/app/src/redux/reducers/notes.js @@ -138,6 +138,7 @@ function editingNote(state={}, action) { }, }; case CREATE_NOTE: + case UPDATE_NOTE: case REMOVE_NOTE: return Object.entries(state).reduce((result, [key, value]) => { if (action.submissionID !== parseInt(key)) { 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 96713ae0233b64cb70e961ae59fe5715e80da0f3..355edae6b0614d19a21ead27d31ba18e9bd52329 100644 --- a/opentech/static_src/src/app/src/redux/reducers/submissions.js +++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js @@ -13,7 +13,7 @@ import { } from '@actions/submissions'; import { CREATE_NOTE, UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes' -import { REMOVE_NOTE_FROM_SUBMISSION } from '@actions/submissions' + function submission(state={comments: []}, action) { switch(action.type) { @@ -61,9 +61,16 @@ function submission(state={comments: []}, action) { isExecutingAction: false, isExecutingActionErrored: true, executionActionError: action.error - } - case CREATE_NOTE: + }; case UPDATE_NOTE: + return { + ...state, + comments: [ + action.data.id, + ...(state.comments.filter(comment => comment !== action.note.id) || []), + ] + }; + case CREATE_NOTE: return { ...state, comments: [ @@ -71,11 +78,6 @@ function submission(state={comments: []}, action) { ...(state.comments || []), ] }; - case REMOVE_NOTE_FROM_SUBMISSION: - return { - ...state, - comments: state.comments.filter(comment => comment !== action.note.id) - } default: return state; } @@ -92,7 +94,6 @@ function submissionsByID(state = {}, action) { case UPDATE_NOTES: case START_EXECUTING_SUBMISSION_ACTION: case FAIL_EXECUTING_SUBMISSION_ACTION: - case REMOVE_NOTE_FROM_SUBMISSION: return { ...state, [action.submissionID]: submission(state[action.submissionID], action),