diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index 611b64d8dbdb713a1b58cbfbea8c4a0b3b0d7263..a2e4499a799630dc41ed2063b8af0db8e560c110 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -303,11 +303,12 @@ class SubmissionsByStatus(BaseAdminSubmissionsTable, DelegateableListView): def dispatch(self, request, *args, **kwargs): self.status = kwargs.get('status') - status_data = self.status_mapping[self.status] + try: + status_data = self.status_mapping[self.status] + except KeyError: + raise Http404(_("No statuses match the requested value")) self.status_name = status_data['name'] self.statuses = status_data['statuses'] - if self.status not in self.status_mapping: - raise Http404(_("No statuses match the requested value")) return super().dispatch(request, *args, **kwargs) def get_filterset_kwargs(self, filterset_class, **kwargs): diff --git a/opentech/static_src/src/app/src/components/DetailView/index.js b/opentech/static_src/src/app/src/components/DetailView/index.js index 76fcd36830e116c12346a82706411b0ee544558a..682d3d42977a19828eb9e2c50c21e9cec049ea8b 100644 --- a/opentech/static_src/src/app/src/components/DetailView/index.js +++ b/opentech/static_src/src/app/src/components/DetailView/index.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React from 'react' import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { withWindowSizeListener } from 'react-window-size-listener'; @@ -12,80 +12,77 @@ import FullScreenLoadingPanel from '@components/FullScreenLoadingPanel'; import './style.scss'; -class DetailView extends Component { - static propTypes = { - listing: PropTypes.element.isRequired, - showSubmision: PropTypes.bool, - windowSize: PropTypes.objectOf(PropTypes.number), - clearSubmission: PropTypes.func.isRequired, - isLoading: PropTypes.bool, - errorMessage: PropTypes.string, - isEmpty: PropTypes.bool, - isErrored: PropTypes.bool, - }; +const DetailView = props => { + const isMobile = (width) => (width ? width : props.windowSize.windowWidth) < 1024 - isMobile = (width) => (width ? width : this.props.windowSize.windowWidth) < 1024 + const renderDisplay = () => <DisplayPanel /> - renderDisplay () { - return <DisplayPanel /> - } + const { listing, isLoading, isErrored, isEmpty, showSubmision, errorMessage } = props - render() { - const { listing, isLoading, isErrored, isEmpty, showSubmision, errorMessage } = this.props; + if (isErrored) { + return ( + <div className="loading-panel"> + <h5>Something went wrong!</h5> + <p>{errorMessage}</p> + </div> + ) + } else if (!isLoading && isEmpty) { + return ( + <div className="loading-panel"> + <h5>No submissions available</h5> + </div> + ) + } - if (isErrored) { - return ( - <div className="loading-panel"> - <h5>Something went wrong!</h5> - <p>{errorMessage}</p> - </div> - ) - } else if (!isLoading && isEmpty) { - return ( - <div className="loading-panel"> - <h5>No submissions available</h5> - </div> - ) - } + if (!props.windowSize.windowWidth) { + return null + } - if (!this.props.windowSize.windowWidth) { - return null - } + let activeDisplay - if (this.isMobile()) { - var activeDisplay; - if (showSubmision) { - activeDisplay = ( - <SlideInRight key={"display"}> - { this.renderDisplay() } - </SlideInRight> - ) - } else { - activeDisplay = ( - <SlideOutLeft key={"listing"}> - { React.cloneElement(listing, { shouldSelectFirst: false }) } - </SlideOutLeft> - ) - } + if (isMobile()) { + if (showSubmision) { + activeDisplay = ( + <SlideInRight key={"display"}> + { renderDisplay() } + </SlideInRight> + ) } else { activeDisplay = ( - <> - {listing} - {this.renderDisplay()} - </> + <SlideOutLeft key={"listing"}> + { React.cloneElement(listing, { shouldSelectFirst: false }) } + </SlideOutLeft> ) } - return ( + } else { + activeDisplay = ( <> - {isLoading && - <FullScreenLoadingPanel /> - } - <div className="detail-view"> - {activeDisplay} - </div> + {listing} + {renderDisplay()} </> ) } + return ( + <> + {isLoading && + <FullScreenLoadingPanel /> + } + <div className="detail-view"> + {activeDisplay} + </div> + </> + ) +} + +DetailView.propTypes = { + listing: PropTypes.element.isRequired, + showSubmision: PropTypes.bool, + windowSize: PropTypes.objectOf(PropTypes.number), + clearSubmission: PropTypes.func.isRequired, + isLoading: PropTypes.bool, + errorMessage: PropTypes.string, + isEmpty: PropTypes.bool, + isErrored: PropTypes.bool, } const mapDispatchToProps = { diff --git a/opentech/static_src/src/app/src/components/MessageBar/index.js b/opentech/static_src/src/app/src/components/MessageBar/index.js index e60dfcb47236e8675841e81a9ce834a7de8ca155..53ec08acd79a2f18689ee02d392c462f16cb2954 100644 --- a/opentech/static_src/src/app/src/components/MessageBar/index.js +++ b/opentech/static_src/src/app/src/components/MessageBar/index.js @@ -3,14 +3,14 @@ import PropTypes from 'prop-types' import { MESSAGE_TYPES } from '@actions/messages' -const MessageBar = ({ message, type, onDismiss }) => { - const modifierClass = type ? `messages__text--${type}` : ''; +const MessageBar = ({ message, type, onDismiss, dismissMessage='OK' }) => { + const modifierClass = type ? `messages__text--${type}` : ''; return ( <li className={`messages__text ${modifierClass}`}> <div className="messages__inner"> <p className="messages__copy">{message}</p> - {onDismiss && <button className="button messages__button" onClick={onDismiss}>Ok</button>} + {onDismiss && <button className="button messages__button" onClick={onDismiss}>{dismissMessage}</button>} </div> </li> ) @@ -20,6 +20,7 @@ MessageBar.propTypes = { type: PropTypes.oneOf(Object.values(MESSAGE_TYPES)), message: PropTypes.string, onDismiss: PropTypes.func, + dismissMessage: PropTypes.string, } export default MessageBar diff --git a/opentech/static_src/src/app/src/components/MessagesList/index.js b/opentech/static_src/src/app/src/components/MessagesList/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ddad485fe6a98041844e07b7604ff1e91be1745d --- /dev/null +++ b/opentech/static_src/src/app/src/components/MessagesList/index.js @@ -0,0 +1,18 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import MessageBar from '@components/MessageBar' + +const MessagesList = ({ children }) => { + return ( + <ul className="messages"> + { children } + </ul> + ) +} + +MessagesList.propTypes = { + children: PropTypes.oneOfType([PropTypes.arrayOf(MessageBar), MessageBar]) +} + +export default MessagesList diff --git a/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js index a3a79823d4ccd89b1021c3b8d7044d361249422e..bee7f2646bd304414b2942928b59a505e7f827c5 100644 --- a/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js +++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js @@ -80,6 +80,7 @@ const answerTypes = { 'rich_text': RichTextAnswer, 'address': AddressAnswer, 'category': BasicListAnswer, + // Files 'file': FileAnswer, 'multi_file': MultiFileAnswer, diff --git a/opentech/static_src/src/app/src/containers/ByRoundListing.js b/opentech/static_src/src/app/src/containers/ByRoundListing.js index 8d0a298bab09280c0fdd6f6e55501639665d488c..e9aaebf1a01c572f974340cda1a4852998c6a425 100644 --- a/opentech/static_src/src/app/src/containers/ByRoundListing.js +++ b/opentech/static_src/src/app/src/containers/ByRoundListing.js @@ -60,7 +60,8 @@ class ByRoundListing extends React.Component { const { isLoading, rounds, submissions } = this.props; if (isLoading) return [] - return submissions.map(submission => submission.round) + return submissions.sort((a, b) => a.id - b.id ) + .map(submission => submission.round) .filter((round, index, arr) => arr.indexOf(round) === index) .map((round, i) => ({ display: rounds[parseInt(round)].title, diff --git a/opentech/static_src/src/app/src/containers/ByStatusListing.js b/opentech/static_src/src/app/src/containers/ByStatusListing.js index 99c565d6fbf279cc5e85ea84a652bc09e44d1269..3f2b7f683dc2703b7138ec186f954f940dfab6c0 100644 --- a/opentech/static_src/src/app/src/containers/ByStatusListing.js +++ b/opentech/static_src/src/app/src/containers/ByStatusListing.js @@ -57,7 +57,7 @@ class ByStatusListing extends React.Component { const slugify = value => value.toLowerCase().replace(/\s/g, '-') const workflow = round.workflow const order = workflow.reduce((accumulator, {display, value}, idx) => { - const key = slugify(value); + const key = slugify(display); const existing = accumulator[key] || {} const existingValues = existing.values || [] const position = existing.position || idx diff --git a/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js b/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js index 4be90e1365997a9df7a230e1196116586a74b209..246514a8125164534e93b2c491f715c68028923a 100644 --- a/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js +++ b/opentech/static_src/src/app/src/containers/CurrentSubmissionDisplay.js @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react' import PropTypes from 'prop-types'; import { connect } from 'react-redux' +import useInterval from '@rooks/use-interval' import { loadCurrentSubmission } from '@actions/submissions' import { @@ -8,36 +9,89 @@ import { getCurrentSubmissionID, } from '@selectors/submissions' import SubmissionDisplay from '@components/SubmissionDisplay'; +import MessagesList from '@components/MessagesList' +import MessageBar from '@components/MessageBar' + const loadData = props => { - props.loadCurrentSubmission(['questions']) + return props.loadCurrentSubmission(['questions'], { bypassCache: true }) +} +const hasChanged = (prevSubmission, submission, keys) => { + return keys.some(key => { + return JSON.stringify(prevSubmission[key]) !== JSON.stringify(submission[key]) + }) } -class CurrentSubmissionDisplay extends React.Component { - static propTypes = { - submission: PropTypes.object, - submissionID: PropTypes.number, - } +const hasContentUpdated = (prevSubmission, submission) => { + return hasChanged(prevSubmission, submission, ['metaQuestions', 'questions']) - componentDidMount() { - loadData(this.props) - } +} + + +const CurrentSubmissionDisplay = props => { + const { submission, submissionID } = props + + const { start, stop } = useInterval(() => loadData(props), 30000) + + const [localSubmission, setSubmission] = useState(undefined); + const [updated, setUpdated] = useState(false); + const [updateMessage, setUpdateMessage] = useState('') + + // Load newly selected submission. + useEffect(() => { + setUpdated(false) + loadData(props) + start() + return () => stop() + }, [submissionID]) - componentDidUpdate(prevProps) { - if (this.props.submissionID !== prevProps.submissionID ) { - loadData(this.props) + // Determine if the submission has been updated by someone else. + useEffect(() => { + if (!submission || !submission.questions || submission.isFetching) { + return; } + + if (!localSubmission || localSubmission.id !== submissionID) { + setSubmission(submission) + } else if (hasContentUpdated(localSubmission, submission)) { + setUpdated(true) + setUpdateMessage('The contents of this application have been changed by someone else.') + } + }, [submission]) + + const handleUpdateSubmission = () => { + setSubmission(submission) + setUpdated(false) + } + + if ( !localSubmission ) { + return <p>Loading</p> } - render () { - const { submission } = this.props - return <SubmissionDisplay - submission={submission} - isLoading={!submission || submission.isFetching} - isError={submission && submission.isErrored} /> + const renderUpdatedMessage = () =>{ + return <MessagesList> + <MessageBar + type='info' + message={updateMessage} + onDismiss={handleUpdateSubmission} + dismissMessage="Show Updated" + /> + </MessagesList> } + return <> + {updated && renderUpdatedMessage()} + <SubmissionDisplay + submission={localSubmission} + isLoading={!localSubmission || localSubmission.isFetching} + isError={localSubmission && localSubmission.isErrored} /> + </> +} + +CurrentSubmissionDisplay.propTypes = { + submission: PropTypes.object, + submissionID: PropTypes.number, } const mapStateToProps = state => ({ @@ -45,5 +99,9 @@ const mapStateToProps = state => ({ submission: getCurrentSubmission(state), }) +const mapDispatchToProps = dispatch => ({ + loadCurrentSubmission: (fields, options) => dispatch(loadCurrentSubmission(fields, options)) +}) + -export default connect(mapStateToProps, {loadCurrentSubmission})(CurrentSubmissionDisplay) +export default connect(mapStateToProps, mapDispatchToProps)(CurrentSubmissionDisplay) diff --git a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js index 291aaff84d61973de9509cfdf1b515129447698f..cbcb98f73d0d9ba40273568dd5235a4093b01dfc 100644 --- a/opentech/static_src/src/app/src/containers/DisplayPanel/index.js +++ b/opentech/static_src/src/app/src/containers/DisplayPanel/index.js @@ -1,89 +1,126 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { withWindowSizeListener } from 'react-window-size-listener'; +import React, { useEffect, useState } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { withWindowSizeListener } from 'react-window-size-listener' -import { clearCurrentSubmission } from '@actions/submissions'; +import { MESSAGE_TYPES, addMessage } from '@actions/messages' +import { clearCurrentSubmission } from '@actions/submissions' import { getCurrentSubmission, getCurrentSubmissionID, -} from '@selectors/submissions'; +} from '@selectors/submissions' import CurrentSubmissionDisplay from '@containers/CurrentSubmissionDisplay' import ReviewInformation from '@containers/ReviewInformation' -import AddNoteForm from '@containers/AddNoteForm'; -import NoteListing from '@containers/NoteListing'; -import StatusActions from '@containers/StatusActions'; +import AddNoteForm from '@containers/AddNoteForm' +import NoteListing from '@containers/NoteListing' +import StatusActions from '@containers/StatusActions' import Tabber, {Tab} from '@components/Tabber' import SubmissionLink from '@components/SubmissionLink'; -import './style.scss'; - -class DisplayPanel extends React.Component { - static propTypes = { - submissionID: PropTypes.number, - loadSubmission: PropTypes.func, - clearSubmission: PropTypes.func.isRequired, - windowSize: PropTypes.objectOf(PropTypes.number) - }; - - render() { - const { windowSize: { windowWidth: width }, submissionID, clearSubmission } = this.props; - const isMobile = width < 1024; - - const submission = <CurrentSubmissionDisplay /> - - let tabs = [ - <Tab button="Notes" key="note"> - <NoteListing submissionID={submissionID} /> - <AddNoteForm submissionID={submissionID} /> - </Tab>, - <Tab button="Status" key="status"> - <StatusActions submissionID={submissionID} /> - <ReviewInformation submissionID={submissionID} /> - <SubmissionLink submissionID={submissionID} /> - </Tab> - ] - if ( isMobile ) { - tabs = [ - <Tab button="Back" key="back" handleClick={ clearSubmission } />, - <Tab button="Application" key="application"> - { submission } - </Tab>, - ...tabs - ] +import './style.scss' + + +const DisplayPanel = props => { + const { submissionID, submission, addMessage} = props + const [ currentStatus, setCurrentStatus ] = useState(undefined) + const [ localSubmissionID, setLocalSubmissionID ] = useState(submissionID) + + useEffect(() => { + setCurrentStatus(undefined) + setLocalSubmissionID(submissionID) + }, [submissionID]) + + useEffect(() => { + if (localSubmissionID !== submissionID) { + return } - return ( - <div className="display-panel"> - { !isMobile && ( - <div className="display-panel__column"> - <div className="display-panel__header display-panel__header--spacer"></div> - <div className="display-panel__body display-panel__body--center"> - { submission } - </div> - </div> - )} + if (!submission || !submission.status) { + setCurrentStatus(undefined) + return + } + + const { status, changedLocally } = submission + + if (currentStatus && status !== currentStatus && !changedLocally) { + addMessage( + 'The status of this application has changed by another user.', + MESSAGE_TYPES.INFO + ) + } + + setCurrentStatus(status) + }) + + const { windowSize: {windowWidth: width} } = props; + const { clearSubmission } = props; + + const isMobile = width < 1024; + const submissionLink = "/apply/submissions/" + submissionID + "/"; + + let tabs = [ + <Tab button="Notes" key="note"> + <NoteListing submissionID={submissionID} /> + <AddNoteForm submissionID={submissionID} /> + </Tab>, + <Tab button="Status" key="status"> + <StatusActions submissionID={submissionID} /> + <ReviewInformation submissionID={submissionID} /> + <SubmissionLink submissionID={submissionID} /> + </Tab> + ] + + if ( isMobile ) { + tabs = [ + <Tab button="Back" key="back" handleClick={ clearSubmission } />, + <Tab button="Application" key="application"> + <CurrentSubmissionDisplay /> + </Tab>, + ...tabs + ] + } + + return ( + <div className="display-panel"> + { !isMobile && ( <div className="display-panel__column"> - <div className="display-panel__body"> - <Tabber> - { tabs } - </Tabber> + <div className="display-panel__header display-panel__header--spacer"></div> + <div className="display-panel__body display-panel__body--center"> + <a target="_blank" rel="noopener noreferrer" href={ submissionLink }>Open in new tab</a> + <CurrentSubmissionDisplay /> </div> </div> + )} + <div className="display-panel__column"> + <div className="display-panel__body"> + <Tabber> + { tabs } + </Tabber> + </div> </div> + </div> - ) - } + ) +} + +DisplayPanel.propTypes = { + submission: PropTypes.object, + submissionID: PropTypes.number, + loadSubmission: PropTypes.func, + clearSubmission: PropTypes.func.isRequired, + windowSize: PropTypes.objectOf(PropTypes.number), + addMessage: PropTypes.func, } const mapStateToProps = state => ({ submissionID: getCurrentSubmissionID(state), submission: getCurrentSubmission(state), -}); +}) const mapDispatchToProps = { - clearSubmission: clearCurrentSubmission + clearSubmission: clearCurrentSubmission, + addMessage: addMessage, } export default connect(mapStateToProps, mapDispatchToProps)(withWindowSizeListener(DisplayPanel)); diff --git a/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js index a3a75a0798ac192f13c7aa61d5ae73d2b1092ded..a5492c4afdc14f771b14db0b0a500ea8ab6151ea 100644 --- a/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js +++ b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js @@ -1,13 +1,13 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from 'react' +import PropTypes from 'prop-types' import { connect } from 'react-redux' -import DetailView from '@components/DetailView'; -import ByRoundListing from '@containers/ByRoundListing'; +import DetailView from '@components/DetailView' +import ByRoundListing from '@containers/ByRoundListing' import { getRoundsFetching, getRoundsErrored, -} from '@selectors/rounds'; +} from '@selectors/rounds' import { getCurrentStatusesSubmissions, getCurrentSubmissionID, @@ -17,32 +17,31 @@ import { getByStatusesError, } from '@selectors/statuses'; +const GroupByRoundDetailView = props => { + const listing = <ByRoundListing submissionStatuses={props.submissionStatuses} /> + const { isLoading, isErrored, submissions, submissionID, errorMessage } = props + const isEmpty = submissions.length === 0 + const activeSubmision = !!submissionID -class GroupByRoundDetailView extends React.Component { - static propTypes = { - submissions: PropTypes.arrayOf(PropTypes.object), - submissionID: PropTypes.number, - isLoading: PropTypes.bool, - isErrored: PropTypes.bool, - errorMessage: PropTypes.string, - }; - - render() { - const { isLoading, isErrored, submissions, submissionID, errorMessage } = this.props; - const isEmpty = submissions.length === 0; - const activeSubmision = !!submissionID; + return ( + <DetailView + isEmpty={isEmpty} + listing={listing} + isLoading={isLoading} + showSubmision={activeSubmision} + isErrored={isErrored} + errorMessage={errorMessage} + /> + ) +} - return ( - <DetailView - isEmpty={isEmpty} - listing={<ByRoundListing />} - isLoading={isLoading} - showSubmision={activeSubmision} - isErrored={isErrored} - errorMessage={errorMessage || "Something went wrong"} - /> - ); - } +GroupByRoundDetailView.propTypes = { + submissionStatuses: PropTypes.arrayOf(PropTypes.string), + submissions: PropTypes.arrayOf(PropTypes.object), + submissionID: PropTypes.number, + isLoading: PropTypes.bool, + isErrored: PropTypes.bool, + errorMessage: PropTypes.string, } const mapStateToProps = (state, ownProps) => ({ @@ -54,6 +53,5 @@ const mapStateToProps = (state, ownProps) => ({ submissionID: getCurrentSubmissionID(state), }) -export default connect( - mapStateToProps, -)(GroupByRoundDetailView); + +export default connect(mapStateToProps)(GroupByRoundDetailView) diff --git a/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js b/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js index e28f0420ea45cf07dd23126f38606a38246116fe..0b1be3df2e6b5de57aafed9166813b1d605be276 100644 --- a/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js +++ b/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js @@ -1,13 +1,13 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from 'react' +import PropTypes from 'prop-types' import { connect } from 'react-redux' -import DetailView from '@components/DetailView'; -import ByStatusListing from '@containers/ByStatusListing'; - +import DetailView from '@components/DetailView' +import ByStatusListing from '@containers/ByStatusListing' import { getSubmissionsByRoundError, getCurrentRoundSubmissions, + getCurrentSubmission, getCurrentSubmissionID, getSubmissionErrorState, } from '@selectors/submissions'; @@ -16,33 +16,33 @@ import { } from '@selectors/rounds'; -class GroupByStatusDetailView extends React.Component { - static propTypes = { - submissions: PropTypes.arrayOf(PropTypes.object), - submissionID: PropTypes.number, - round: PropTypes.object, - isErrored: PropTypes.bool, - errorMessage: PropTypes.string, - }; - - render() { - const listing = <ByStatusListing />; - const { round, isErrored, submissions, submissionID, errorMessage } = this.props; - const isLoading = !round || (round && (round.isFetching || round.submissions.isFetching)) - const isEmpty = submissions.length === 0; - const activeSubmision = !!submissionID; - - return ( - <DetailView - isErrored={isErrored} - listing={listing} - isEmpty={isEmpty} - isLoading={isLoading} - showSubmision={activeSubmision} - errorMessage={errorMessage || 'Fetching failed.'} - /> - ); - } +const GroupByStatusDetailView = ({ currentSubmission, round, isErrored, submissions, submissionID, errorMessage }) => { + const listing = <ByStatusListing /> + const isLoading = !round || (round && (round.isFetching || round.submissions.isFetching)) + const isEmpty = submissions.length === 0 + const activeSubmision = !!submissionID + + return ( + <DetailView + isErrored={isErrored} + listing={listing} + isEmpty={isEmpty} + isLoading={isLoading} + showSubmision={activeSubmision} + errorMessage={errorMessage || 'Fetching failed.'} + /> + ); +} + +GroupByStatusDetailView.propTypes = { + submissions: PropTypes.arrayOf(PropTypes.object), + submissionID: PropTypes.number, + round: PropTypes.object, + isErrored: PropTypes.bool, + errorMessage: PropTypes.string, + currentSubmission: PropTypes.shape({ + status: PropTypes.string + }), } const mapStateToProps = state => ({ @@ -50,9 +50,11 @@ const mapStateToProps = state => ({ isErrored: getSubmissionErrorState(state), errorMessage: getSubmissionsByRoundError(state), submissions: getCurrentRoundSubmissions(state), + currentSubmission: getCurrentSubmission(state), submissionID: getCurrentSubmissionID(state), }) -export default connect( - mapStateToProps, -)(GroupByStatusDetailView); + +export default connect(mapStateToProps)( + GroupByStatusDetailView +) diff --git a/opentech/static_src/src/app/src/containers/MessagesContainer.js b/opentech/static_src/src/app/src/containers/MessagesContainer.js index 535a43a359f2ca80e485690d6133bef7dce3a1d3..1ff163bb146116692b0b51683ecc924f6d8068c3 100644 --- a/opentech/static_src/src/app/src/containers/MessagesContainer.js +++ b/opentech/static_src/src/app/src/containers/MessagesContainer.js @@ -3,17 +3,18 @@ import { connect } from 'react-redux' import PropTypes from 'prop-types' import MessageBar from '@components/MessageBar' +import MessagesList from '@components/MessagesList' import { getMessages } from '@selectors/messages' import { dismissMessage } from '@actions/messages' const MessagesContainer = ({ messages, dismiss }) => { return ( - <ul className="messages"> + <MessagesList> {Object.values(messages).map(({ message, type, id}) => <MessageBar key={id} message={message} type={type} onDismiss={() => dismiss(id)} /> )} - </ul> + </MessagesList> ) } 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 005860211cda3e7a5e4b41ff3949f2d711410468..6ba68106ae1e4bfdc73ab228d762e359b6d56a0a 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -246,14 +246,14 @@ const fetchSubmission = (submissionID) => ({ submissionID, }) -export const loadCurrentSubmission = (requiredFields=[]) => (dispatch, getState) => { +export const loadCurrentSubmission = (requiredFields=[], { bypassCache = false }) => (dispatch, getState) => { const submissionID = getCurrentSubmissionID(getState()) if ( !submissionID ) { return null } const submission = getCurrentSubmission(getState()) - if (submission && requiredFields.every(key => submission.hasOwnProperty(key))) { + if (!bypassCache && submission && requiredFields.every(key => submission.hasOwnProperty(key))) { return null } @@ -281,4 +281,5 @@ export const executeSubmissionAction = (submissionID, action) => ({ endpoint: api.executeSubmissionAction(submissionID, action), }, submissionID, + changedLocally: true, }) diff --git a/opentech/static_src/src/app/src/redux/reducers/statuses.js b/opentech/static_src/src/app/src/redux/reducers/statuses.js index b0f914d7d054064b4ad5887fd6aed17f487546e6..107f40721ed14563c23f100a83f19e0a9bb989e9 100644 --- a/opentech/static_src/src/app/src/redux/reducers/statuses.js +++ b/opentech/static_src/src/app/src/redux/reducers/statuses.js @@ -5,6 +5,7 @@ import { UPDATE_BY_STATUSES, START_LOADING_BY_STATUSES, FAIL_LOADING_BY_STATUSES, + UPDATE_SUBMISSION, } from '@actions/submissions'; @@ -30,6 +31,16 @@ function submissionsByStatuses(state = {}, action) { return accumulator }, state) }; + case UPDATE_SUBMISSION: + state = Object.entries(state).reduce( + (accumulator, [status, ids]) => { + accumulator[status] = ids.filter(id => id !== action.data.id); + return accumulator; + }, {}); + return { + ...state, + [action.data.status]: [...(state[action.data.status] || []), action.data.id], + }; default: return state } 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 08aab44c57d3e39d366ad80f3d320ecb44b06b44..529239aa11da8aea2572ba8259a315f0b3dbb63f 100644 --- a/opentech/static_src/src/app/src/redux/reducers/submissions.js +++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js @@ -38,6 +38,7 @@ function submission(state={comments: []}, action) { isExecutingAction: false, isExecutingActionErrored: false, executionActionError: undefined, + changedLocally: action.changedLocally === true }; case UPDATE_NOTES: return { diff --git a/package-lock.json b/package-lock.json index 7d30996318d42ab39c0e257f4a7244b65c830b46..7d8bc54bfc3c5cee1c388a6f283f857c50034b95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3073,8 +3073,7 @@ "detect-node": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==" }, "diffie-hellman": { "version": "5.0.3", @@ -6768,6 +6767,11 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -6785,6 +6789,11 @@ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=" + }, "lodash.some": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", diff --git a/package.json b/package.json index afd1acccf7253bed76bf2568c69a64d8bfd10a22..2a88da568a139251abb5a585cb143bd0c0006ee5 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@svgr/webpack": "^4.1.0", "connected-react-router": "^6.3.1", "del": "^3.0.0", + "detect-node": "^2.0.4", "gulp": "^4.0.0", "gulp-babel": "^8.0.0", "gulp-clean-css": "^3.10.0", @@ -27,6 +28,8 @@ "gulp-uglify": "^3.0.1", "humps": "^2.0.1", "js-cookie": "^2.2.0", + "lodash.isequal": "^4.5.0", + "lodash.pick": "^4.4.0", "node-sass-import-once": "^1.2.0", "prop-types": "^15.6.2", "react": "^16.8.1",