diff --git a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js index 33a3c41831079424ba5c05019a7c9c4f6b83f2d8..26cdc348fb7729ea6c49994f66fe16f838f5e667 100644 --- a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js +++ b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js @@ -20,18 +20,18 @@ class SubmissionsByRoundApp extends React.Component { } render() { - return <SwitcherApp + return ( + <SwitcherApp detailComponent={<GroupByStatusDetailView />} switcherSelector={'submissions-by-round-app-react-switcher'} - pageContent={this.props.pageContent} />; + pageContent={this.props.pageContent} /> + ) } } const mapDispatchToProps = dispatch => { return { - setSubmissionRound: id => { - dispatch(setCurrentSubmissionRound(id)); - }, + setSubmissionRound: id => {dispatch(setCurrentSubmissionRound(id));}, } }; diff --git a/opentech/static_src/src/app/src/SubmissionsByStatusApp.js b/opentech/static_src/src/app/src/SubmissionsByStatusApp.js index 785d1c2ffdd6373d27d69aa9a856a24997a4b87f..90c6b8989f4665e129841af6da44a87134142410 100644 --- a/opentech/static_src/src/app/src/SubmissionsByStatusApp.js +++ b/opentech/static_src/src/app/src/SubmissionsByStatusApp.js @@ -2,22 +2,38 @@ import React from 'react'; import PropTypes from 'prop-types'; import SwitcherApp from './SwitcherApp'; import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux' import GroupByRoundDetailView from '@containers/GroupByRoundDetailView'; +import { setCurrentStatuses } from '@actions/submissions'; class SubmissionsByStatusApp extends React.Component { static propTypes = { pageContent: PropTypes.node.isRequired, statuses: PropTypes.arrayOf(PropTypes.string), + setStatuses: PropTypes.func.isRequired, }; + componentDidMount() { + this.props.setStatuses(this.props.statuses); + } + render() { return <SwitcherApp - detailComponent={<GroupByRoundDetailView submissionStatuses={this.props.statuses} />} + detailComponent={<GroupByRoundDetailView />} switcherSelector={'submissions-by-status-app-react-switcher'} pageContent={this.props.pageContent} />; } } -export default hot(module)(SubmissionsByStatusApp); +const mapDispatchToProps = dispatch => { + return { + setStatuses: statuses => {dispatch(setCurrentStatuses(statuses));}, + } +}; + + +export default hot(module)( + connect(null, mapDispatchToProps)(SubmissionsByStatusApp) +); diff --git a/opentech/static_src/src/app/src/SwitcherApp.js b/opentech/static_src/src/app/src/SwitcherApp.js index d88c25d402816437453af93cd86c3636d1f1ff87..f895b79e3451fec906783b906bfcd1cb31fbd42e 100644 --- a/opentech/static_src/src/app/src/SwitcherApp.js +++ b/opentech/static_src/src/app/src/SwitcherApp.js @@ -1,21 +1,59 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { connect } from 'react-redux' import Switcher from '@components/Switcher'; import MessagesContainer from '@containers/MessagesContainer' +import { + clearCurrentSubmissionParam, + loadSubmissionFromURL, + setCurrentSubmissionParam, +} from '@actions/submissions'; -export default class SwitcherApp extends React.Component { + +class SwitcherApp extends React.Component { static propTypes = { pageContent: PropTypes.node.isRequired, detailComponent: PropTypes.node.isRequired, switcherSelector: PropTypes.string.isRequired, + startOpen: PropTypes.bool, + processParams: PropTypes.func.isRequired, + searchParam: PropTypes.string, + setParams: PropTypes.func.isRequired, + clearParams: PropTypes.func.isRequired, + }; + + state = { + detailOpened: false, + mounting: true, }; + componentDidMount() { + this.setState({ + mounting: false + }) + + const success = this.props.processParams(this.props.searchParam) + if (success) { + this.openDetail() + } + } + + componentDidUpdate(prevProps) { + if (prevProps.searchParam !== this.props.searchParam) { + const success = this.props.processParams(this.props.searchParam) - state = { detailOpened: false }; + if (!success) { + this.closeDetail() + } else { + this.openDetail() + } + } + } openDetail = () => { document.body.classList.add('app-open'); + this.props.setParams(); this.setState(state => ({ style: { ...state.style, display: 'none' } , detailOpened: true, @@ -24,6 +62,7 @@ export default class SwitcherApp extends React.Component { closeDetail = () => { document.body.classList.remove('app-open'); + this.props.clearParams(); this.setState(state => { const newStyle = { ...state.style }; delete newStyle.display; @@ -35,6 +74,9 @@ export default class SwitcherApp extends React.Component { } render() { + if ( this.state.mounting ) { + return null + } return ( <> <MessagesContainer /> @@ -47,3 +89,15 @@ export default class SwitcherApp extends React.Component { ) } } + +const mapStateToProps = (state) => ({ + searchParam: state.router.location.search +}) + +const mapDispatchToProps = dispatch => ({ + processParams: params => dispatch(loadSubmissionFromURL(params)), + clearParams: () => dispatch(clearCurrentSubmissionParam()), + setParams: () => dispatch(setCurrentSubmissionParam()), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(SwitcherApp); diff --git a/opentech/static_src/src/app/src/components/GroupedListing/index.js b/opentech/static_src/src/app/src/components/GroupedListing/index.js index be5893d763fa343759807c7c8a48d04738b7e84c..e1e1143308958a6f9ffb82a6f0a3e7916fe4717c 100644 --- a/opentech/static_src/src/app/src/components/GroupedListing/index.js +++ b/opentech/static_src/src/app/src/components/GroupedListing/index.js @@ -19,7 +19,9 @@ export default class GroupedListing extends React.Component { order: PropTypes.arrayOf(PropTypes.shape({ key: PropTypes.string.isRequired, display: PropTypes.string.isRequired, - values: PropTypes.arrayOf(PropTypes.string), + values: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + ) })), onItemSelection: PropTypes.func, shouldSelectFirst: PropTypes.bool, @@ -29,7 +31,6 @@ export default class GroupedListing extends React.Component { shouldSelectFirst: true, } - state = { orderedItems: [], }; @@ -46,29 +47,17 @@ export default class GroupedListing extends React.Component { this.dropdownContainerHeight = this.dropdownContainer.offsetHeight; } - shouldComponentUpdate(nextProps, nextState) { - const propsToCheck = ['items', 'isLoading', 'isErrored'] - if ( propsToCheck.some(prop => nextProps[prop] !== this.props[prop])) { - return true - } - if ( nextState.orderedItems !== this.state.orderedItems ) { - return true - } - return false - } - componentDidUpdate(prevProps, prevState) { // Order items - if (this.props.items !== prevProps.items) { + if (this.props.items !== prevProps.items || this.props.order !== prevProps.order) { this.orderItems(); } if ( this.props.shouldSelectFirst ){ - const oldItem = prevProps.activeItem const newItem = this.props.activeItem - // If we have never activated a submission, get the first item - if ( !newItem && !oldItem ) { + // If we dont have an active item, then get one + if ( !newItem ) { const firstGroup = this.state.orderedItems[0] if ( firstGroup && firstGroup.items[0] ) { this.setState({firstUpdate: false}) 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 d270da8d5d091b5a8ca22ee60e16581039677e0b..d362877c05f4a68ff759eba4ec779fefcebd31e6 100644 --- a/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js +++ b/opentech/static_src/src/app/src/components/SubmissionDisplay/answers.js @@ -19,7 +19,7 @@ const ListAnswer = ({Wrapper, answers}) => { ) }; ListAnswer.propTypes = { - Wrapper: PropTypes.element, + Wrapper: PropTypes.func, ...arrayAnswerType, } @@ -43,7 +43,12 @@ const FileAnswer = ({answer}) => ( FileAnswer.propTypes = fileType const MultiFileAnswer = ({answer}) => <ListAnswer Wrapper={FileAnswer} answers={answer} />; -MultiFileAnswer.propTypes = {answer: PropTypes.arrayOf(fileType)} +MultiFileAnswer.propTypes = { + answer: PropTypes.arrayOf(PropTypes.shape({ + filename: PropTypes.string.isRequired, + url:PropTypes.string.isRequired, + })) +} const AddressAnswer = ({answer}) => ( <div>{ diff --git a/opentech/static_src/src/app/src/containers/ByRoundListing.js b/opentech/static_src/src/app/src/containers/ByRoundListing.js index 3319982686c4b26480cee0e5d17ce8737decae57..8d0a298bab09280c0fdd6f6e55501639665d488c 100644 --- a/opentech/static_src/src/app/src/containers/ByRoundListing.js +++ b/opentech/static_src/src/app/src/containers/ByRoundListing.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux' import GroupedListing from '@components/GroupedListing'; import { loadRounds, - loadSubmissionsOfStatuses, + loadSubmissionsForCurrentStatus, setCurrentSubmission, } from '@actions/submissions'; import { @@ -14,11 +14,14 @@ import { getRoundsErrored, } from '@selectors/rounds'; import { - getSubmissionsByGivenStatuses, getCurrentSubmissionID, - getByGivenStatusesError, - getByGivenStatusesLoading, + getCurrentStatusesSubmissions, } from '@selectors/submissions'; +import { + getCurrentStatuses, + getByStatusesLoading, + getByStatusesError, +} from '@selectors/statuses'; const loadData = props => { @@ -28,28 +31,27 @@ const loadData = props => { class ByRoundListing extends React.Component { static propTypes = { - submissionStatuses: PropTypes.arrayOf(PropTypes.string), + statuses: PropTypes.arrayOf(PropTypes.string), loadSubmissions: PropTypes.func, submissions: PropTypes.arrayOf(PropTypes.object), isErrored: PropTypes.bool, setCurrentItem: PropTypes.func, activeSubmission: PropTypes.number, shouldSelectFirst: PropTypes.bool, - rounds: PropTypes.array, + rounds: PropTypes.object, isLoading: PropTypes.bool, errorMessage: PropTypes.string, }; componentDidMount() { - // Update items if round ID is defined. - if ( this.props.submissionStatuses ) { + if ( this.props.statuses) { loadData(this.props) } } componentDidUpdate(prevProps) { - const { submissionStatuses } = this.props; - if (!submissionStatuses.every(v => prevProps.submissionStatuses.includes(v))) { + const { statuses} = this.props; + if (!statuses.every(v => prevProps.statuses.includes(v))) { loadData(this.props) } } @@ -64,7 +66,7 @@ class ByRoundListing extends React.Component { display: rounds[parseInt(round)].title, key: `round-${round}`, position: i, - values: [round], + values: [parseInt(round)], })); } @@ -85,20 +87,20 @@ class ByRoundListing extends React.Component { } } -const mapStateToProps = (state, ownProps) => ({ - submissions: getSubmissionsByGivenStatuses(ownProps.submissionStatuses)(state), - isErrored: getRoundsErrored(state), - errorMessage: getByGivenStatusesError(ownProps.submissionStatuses)(state), +const mapStateToProps = (state) => ({ + statuses: getCurrentStatuses(state), + submissions: getCurrentStatusesSubmissions(state), + isErrored: getRoundsErrored(state) || getByStatusesError(state), isLoading: ( - getByGivenStatusesLoading(ownProps.submissionStatuses)(state) || + getByStatusesLoading(state) || getRoundsFetching(state) ), activeSubmission: getCurrentSubmissionID(state), rounds: getRounds(state), }) -const mapDispatchToProps = (dispatch, ownProps) => ({ - loadSubmissions: () => dispatch(loadSubmissionsOfStatuses(ownProps.submissionStatuses)), +const mapDispatchToProps = (dispatch) => ({ + loadSubmissions: () => dispatch(loadSubmissionsForCurrentStatus()), loadRounds: () => dispatch(loadRounds()), setCurrentItem: id => dispatch(setCurrentSubmission(id)), }); diff --git a/opentech/static_src/src/app/src/containers/ByStatusListing.js b/opentech/static_src/src/app/src/containers/ByStatusListing.js index 6eb98de57a982a2644180d65d3e3b97379bdd963..99c565d6fbf279cc5e85ea84a652bc09e44d1269 100644 --- a/opentech/static_src/src/app/src/containers/ByStatusListing.js +++ b/opentech/static_src/src/app/src/containers/ByStatusListing.js @@ -9,12 +9,14 @@ import { setCurrentSubmission, } from '@actions/submissions'; import { - getCurrentRound, - getCurrentRoundID, getCurrentRoundSubmissions, getCurrentSubmissionID, getSubmissionsByRoundError, } from '@selectors/submissions'; +import { + getCurrentRound, + getCurrentRoundID, +} from '@selectors/rounds'; const loadData = props => { @@ -55,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(display); + const key = slugify(value); const existing = accumulator[key] || {} const existingValues = existing.values || [] const position = existing.position || idx diff --git a/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js index f8811112b88613ea8d31589bd00c9e8c5dd22119..a3a75a0798ac192f13c7aa61d5ae73d2b1092ded 100644 --- a/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js +++ b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js @@ -9,15 +9,17 @@ import { getRoundsErrored, } from '@selectors/rounds'; import { - getByGivenStatusesLoading, - getByGivenStatusesError, - getCurrentRoundSubmissions, + getCurrentStatusesSubmissions, getCurrentSubmissionID, } from '@selectors/submissions'; +import { + getByStatusesLoading, + getByStatusesError, +} from '@selectors/statuses'; + class GroupByRoundDetailView extends React.Component { static propTypes = { - submissionStatuses: PropTypes.arrayOf(PropTypes.string), submissions: PropTypes.arrayOf(PropTypes.object), submissionID: PropTypes.number, isLoading: PropTypes.bool, @@ -26,7 +28,6 @@ class GroupByRoundDetailView extends React.Component { }; render() { - const listing = <ByRoundListing submissionStatuses={this.props.submissionStatuses} />; const { isLoading, isErrored, submissions, submissionID, errorMessage } = this.props; const isEmpty = submissions.length === 0; const activeSubmision = !!submissionID; @@ -34,24 +35,22 @@ class GroupByRoundDetailView extends React.Component { return ( <DetailView isEmpty={isEmpty} - listing={listing} + listing={<ByRoundListing />} isLoading={isLoading} showSubmision={activeSubmision} isErrored={isErrored} - errorMessage={errorMessage || 'Fetching failed.'} + errorMessage={errorMessage || "Something went wrong"} /> ); } } const mapStateToProps = (state, ownProps) => ({ - isErrored: getRoundsErrored(state), - errorMessage: getByGivenStatusesError(ownProps.submissionStatuses)(state) ? "Something went wrong" : "", + isErrored: getRoundsErrored(state) || getByStatusesError(state), isLoading: ( - getByGivenStatusesLoading(ownProps.submissionStatuses)(state) || - getRoundsFetching(state) + getByStatusesLoading(state) || getRoundsFetching(state) ), - submissions: getCurrentRoundSubmissions(state), + submissions: getCurrentStatusesSubmissions(state), submissionID: getCurrentSubmissionID(state), }) diff --git a/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js b/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js index 355979a706e9ac73ba6ec468b3b46013d9d81b5a..e28f0420ea45cf07dd23126f38606a38246116fe 100644 --- a/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js +++ b/opentech/static_src/src/app/src/containers/GroupByStatusDetailView.js @@ -6,12 +6,14 @@ import DetailView from '@components/DetailView'; import ByStatusListing from '@containers/ByStatusListing'; import { - getCurrentRound, getSubmissionsByRoundError, getCurrentRoundSubmissions, getCurrentSubmissionID, getSubmissionErrorState, } from '@selectors/submissions'; +import { + getCurrentRound, +} from '@selectors/rounds'; class GroupByStatusDetailView extends React.Component { 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 e4c283e113786b6c0c0a1d5ec93c62dea239628e..005860211cda3e7a5e4b41ff3949f2d711410468 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -1,15 +1,29 @@ +import { push } from 'connected-react-router' import { CALL_API } from '@middleware/api' import api from '@api'; import { getCurrentSubmission, getCurrentSubmissionID, + getCurrentStatusesSubmissions +} from '@selectors/submissions'; + +import { + getCurrentStatuses, + getSubmissionIDsForCurrentStatuses, +} from '@selectors/statuses'; + +import { + getRounds, getCurrentRoundID, getCurrentRound, getCurrentRoundSubmissionIDs, - getRounds, - getSubmissionsByGivenStatuses, -} from '@selectors/submissions'; +} from '@selectors/rounds'; + +import { + MESSAGE_TYPES, + addMessage, +} from '@actions/messages'; // Round @@ -30,9 +44,10 @@ export const START_LOADING_SUBMISSIONS_BY_ROUND = 'START_LOADING_SUBMISSIONS_BY_ export const FAIL_LOADING_SUBMISSIONS_BY_ROUND = 'FAIL_LOADING_SUBMISSIONS_BY_ROUND'; // Submissions by statuses -export const UPDATE_SUBMISSIONS_BY_STATUSES = 'UPDATE_SUBMISSIONS_BY_STATUSES'; -export const START_LOADING_SUBMISSIONS_BY_STATUSES = 'START_LOADING_SUBMISSIONS_BY_STATUSES'; -export const FAIL_LOADING_SUBMISSIONS_BY_STATUSES = 'FAIL_LOADING_SUBMISSIONS_BY_STATUSES'; +export const SET_CURRENT_STATUSES = "SET_CURRENT_STATUSES_FOR_SUBMISSIONS"; +export const UPDATE_BY_STATUSES = 'UPDATE_SUBMISSIONS_BY_STATUSES'; +export const START_LOADING_BY_STATUSES = 'START_LOADING_SUBMISSIONS_BY_STATUSES'; +export const FAIL_LOADING_BY_STATUSES = 'FAIL_LOADING_SUBMISSIONS_BY_STATUSES'; // Submissions export const SET_CURRENT_SUBMISSION = 'SET_CURRENT_SUBMISSION'; @@ -53,10 +68,65 @@ export const setCurrentSubmissionRound = id => ({ id, }); -export const setCurrentSubmission = id => ({ - type: SET_CURRENT_SUBMISSION, - id, -}); + +export const loadSubmissionFromURL = (params) => (dispatch, getState) => { + const urlParams = new URLSearchParams(params); + if (urlParams.has('submission')) { + const activeId = Number(urlParams.get('submission')); + const submissionID = getCurrentSubmissionID(getState()); + + if (activeId !== null && submissionID !== activeId) { + dispatch(setCurrentSubmission(activeId)); + } + return true; + } + return false; +}; + + + +export const clearCurrentSubmissionParam = () => (dispatch, getState) => { + const state = getState(); + if (state.router.location.search !== '') { + return dispatch(push({search: ''})); + } +}; + + +const setSubmissionParam = (id) => (dispatch, getState) => { + const state = getState(); + const submissionID = getCurrentSubmissionID(state); + + const urlParams = new URLSearchParams(state.router.location.search); + const urlID = Number(urlParams.get('submission')); + + const shouldSet = !urlID && !!id; + const shouldUpdate = id !== null && submissionID !== id && urlID !== id; + + if (shouldSet || shouldUpdate) { + dispatch(push({search: `?submission=${id}`})); + } else if (id === null) { + dispatch(clearCurrentSubmissionParam()); + } + +}; + + +export const setCurrentSubmissionParam = () => (dispatch, getState) => { + const submissionID = getCurrentSubmissionID(getState()); + return dispatch(setSubmissionParam(submissionID)); +}; + + + +export const setCurrentSubmission = id => (dispatch, getState) => { + dispatch(setSubmissionParam(id)); + + return dispatch({ + type: SET_CURRENT_SUBMISSION, + id, + }) +}; export const loadCurrentRound = (requiredFields=[]) => (dispatch, getState) => { @@ -81,6 +151,7 @@ export const loadRounds = () => (dispatch, getState) => { return dispatch(fetchRounds()) } + export const loadCurrentRoundSubmissions = () => (dispatch, getState) => { const state = getState() const submissions = getCurrentRoundSubmissionIDs(state) @@ -89,7 +160,15 @@ export const loadCurrentRoundSubmissions = () => (dispatch, getState) => { return null } - return dispatch(fetchSubmissionsByRound(getCurrentRoundID(state))) + return dispatch(fetchSubmissionsByRound(getCurrentRoundID(state))).then(() => { + const state = getState() + const ids = getCurrentRoundSubmissionIDs(state) + const currentSubmissionID = getCurrentSubmissionID(state) + if (currentSubmissionID !== null && !ids.includes(currentSubmissionID)) { + dispatch(addMessage('The selected submission is not available in this view', MESSAGE_TYPES.WARNING)) + return dispatch(setCurrentSubmission(null)) + } + }) } @@ -118,29 +197,45 @@ const fetchSubmissionsByRound = (roundID) => ({ }) -const fetchSubmissionsByStatuses = statuses => { +export const setCurrentStatuses = (statuses) => (dispatch) => { if(!Array.isArray(statuses)) { throw new Error("Statuses have to be an array of statuses"); } + return dispatch({ + type: SET_CURRENT_STATUSES, + statuses, + }); +}; + + +const fetchSubmissionsByStatuses = (statuses) => { return { [CALL_API]: { - types: [ START_LOADING_SUBMISSIONS_BY_STATUSES, UPDATE_SUBMISSIONS_BY_STATUSES, FAIL_LOADING_SUBMISSIONS_BY_STATUSES], + types: [ START_LOADING_BY_STATUSES, UPDATE_BY_STATUSES, FAIL_LOADING_BY_STATUSES], endpoint: api.fetchSubmissionsByStatuses(statuses), }, statuses, }; }; -export const loadSubmissionsOfStatuses = statuses => (dispatch, getState) => { +export const loadSubmissionsForCurrentStatus = () => (dispatch, getState) => { const state = getState() - const submissions = getSubmissionsByGivenStatuses(statuses)(state) + const submissions = getCurrentStatusesSubmissions(state) if ( submissions && submissions.length !== 0 ) { return null } - return dispatch(fetchSubmissionsByStatuses(statuses)) + return dispatch(fetchSubmissionsByStatuses(getCurrentStatuses(state))).then(() => { + const state = getState() + const ids = getSubmissionIDsForCurrentStatuses(state) + const currentSubmissionID = getCurrentSubmissionID(state) + if (currentSubmissionID !== null && !ids.includes(currentSubmissionID)) { + dispatch(addMessage('The selected submission is not available in this view', MESSAGE_TYPES.WARNING)) + return dispatch(setCurrentSubmission(null)) + } + }) } const fetchSubmission = (submissionID) => ({ diff --git a/opentech/static_src/src/app/src/redux/reducers/index.js b/opentech/static_src/src/app/src/redux/reducers/index.js index b48c3a4cd13e451c4ca840f641391551618d090b..e29835e85bd8a2789b19ef4ff3e2ba128ee74c12 100644 --- a/opentech/static_src/src/app/src/redux/reducers/index.js +++ b/opentech/static_src/src/app/src/redux/reducers/index.js @@ -1,13 +1,17 @@ import { combineReducers } from 'redux' +import { connectRouter } from 'connected-react-router' import submissions from '@reducers/submissions'; import rounds from '@reducers/rounds'; import notes from '@reducers/notes'; import messages from '@reducers/messages'; +import statuses from '@reducers/statuses'; -export default combineReducers({ - messages, +export default (history) => combineReducers({ + router: connectRouter(history), notes, - submissions, + messages, rounds, + statuses, + submissions, }); diff --git a/opentech/static_src/src/app/src/redux/reducers/statuses.js b/opentech/static_src/src/app/src/redux/reducers/statuses.js new file mode 100644 index 0000000000000000000000000000000000000000..b0f914d7d054064b4ad5887fd6aed17f487546e6 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/reducers/statuses.js @@ -0,0 +1,67 @@ +import { combineReducers } from 'redux'; + +import { + SET_CURRENT_STATUSES, + UPDATE_BY_STATUSES, + START_LOADING_BY_STATUSES, + FAIL_LOADING_BY_STATUSES, +} from '@actions/submissions'; + + +function current(state = [], action) { + switch (action.type) { + case SET_CURRENT_STATUSES: + return [...action.statuses] + default: + return state + } +} + +function submissionsByStatuses(state = {}, action) { + switch (action.type) { + case UPDATE_BY_STATUSES: + return { + ...state, + ...action.data.results.reduce((accumulator, submission) => { + const submissions = accumulator[submission.status] || [] + if ( !submissions.includes(submission.id) ) { + accumulator[submission.status] = [...submissions, submission.id] + } + return accumulator + }, state) + }; + default: + return state + } +} + + +function statusFetchingState(state = {isFetching: true, isError: false}, action) { + switch (action.type) { + case FAIL_LOADING_BY_STATUSES: + return { + isFetching: false, + isErrored: true, + }; + case START_LOADING_BY_STATUSES: + return { + isFetching: true, + isErrored: false, + }; + case UPDATE_BY_STATUSES: + return { + isFetching: false, + isErrored: false, + }; + default: + return state + } +} + +const statuses = combineReducers({ + current, + byStatuses: submissionsByStatuses, + fetchingState: statusFetchingState, +}); + +export default statuses; 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 be14aea45886a6e979c85ad13bdc8bab227d4a3f..08aab44c57d3e39d366ad80f3d320ecb44b06b44 100644 --- a/opentech/static_src/src/app/src/redux/reducers/submissions.js +++ b/opentech/static_src/src/app/src/redux/reducers/submissions.js @@ -7,9 +7,7 @@ import { UPDATE_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSION, SET_CURRENT_SUBMISSION, - UPDATE_SUBMISSIONS_BY_STATUSES, - START_LOADING_SUBMISSIONS_BY_STATUSES, - FAIL_LOADING_SUBMISSIONS_BY_STATUSES, + UPDATE_BY_STATUSES, START_EXECUTING_SUBMISSION_ACTION, FAIL_EXECUTING_SUBMISSION_ACTION, } from '@actions/submissions'; @@ -90,7 +88,7 @@ function submissionsByID(state = {}, action) { ...state, [action.submissionID]: submission(state[action.submissionID], action), }; - case UPDATE_SUBMISSIONS_BY_STATUSES: + case UPDATE_BY_STATUSES: case UPDATE_SUBMISSIONS_BY_ROUND: return { ...state, @@ -123,52 +121,9 @@ function currentSubmission(state = null, action) { } -function submissionsByStatuses(state = {}, action) { - switch (action.type) { - case UPDATE_SUBMISSIONS_BY_STATUSES: - return { - ...state, - ...action.data.results.reduce((accumulator, submission) => { - const submissions = accumulator[submission.status] || [] - if ( !submissions.includes(submission.id) ) { - accumulator[submission.status] = [...submissions, submission.id] - } - return state - }, state) - }; - default: - return state - } -} - - -function submissionsFetchingState(state = {isFetching: true, isError: false}, action) { - switch (action.type) { - case FAIL_LOADING_SUBMISSIONS_BY_STATUSES: - return { - isFetching: false, - isErrored: true, - }; - case START_LOADING_SUBMISSIONS_BY_STATUSES: - return { - isFetching: true, - isErrored: false, - }; - case UPDATE_SUBMISSIONS_BY_STATUSES: - return { - isFetching: true, - isErrored: false, - }; - default: - return state - } -} - const submissions = combineReducers({ byID: submissionsByID, current: currentSubmission, - byStatuses: submissionsByStatuses, - fetchingState: submissionsFetchingState, }); export default submissions; diff --git a/opentech/static_src/src/app/src/redux/selectors/statuses.js b/opentech/static_src/src/app/src/redux/selectors/statuses.js new file mode 100644 index 0000000000000000000000000000000000000000..ff17929c7c7242b7c34cd566814fb1d6416ae6f1 --- /dev/null +++ b/opentech/static_src/src/app/src/redux/selectors/statuses.js @@ -0,0 +1,33 @@ +import { createSelector } from 'reselect'; + + +const getCurrentStatuses = state => state.statuses.current; + +const getStatusesFetchingState = state => state.statuses.fetchingState; + +const getSubmissionsByStatuses = state => state.statuses.byStatuses; + + +const getSubmissionIDsForCurrentStatuses = createSelector( + [ getSubmissionsByStatuses, getCurrentStatuses ], + (grouped, current) => { + return current.reduce((acc, status) => acc.concat(grouped[status] || []), []) + } +); + +const getByStatusesError = createSelector( + [ getStatusesFetchingState ], + state => state.isErrored === true +); + +const getByStatusesLoading = createSelector( + [ getStatusesFetchingState ], + state => state.isFetching === true +); + +export { + getCurrentStatuses, + getByStatusesLoading, + getByStatusesError, + getSubmissionIDsForCurrentStatuses, +} diff --git a/opentech/static_src/src/app/src/redux/selectors/submissions.js b/opentech/static_src/src/app/src/redux/selectors/submissions.js index 643684d2e8d4648477eb560e69e25be39dde5f11..6d3ee1ad5a70cb5d3769a05960de052d9d78e460 100644 --- a/opentech/static_src/src/app/src/redux/selectors/submissions.js +++ b/opentech/static_src/src/app/src/redux/selectors/submissions.js @@ -1,43 +1,28 @@ import { createSelector } from 'reselect'; import { - getCurrentRound, - getCurrentRoundID, getCurrentRoundSubmissionIDs, - getRounds, } from '@selectors/rounds'; -const getSubmissions = state => state.submissions.byID; - -const getSubmissionsByStatuses = state => state.submissions.byStatuses; +import { + getSubmissionIDsForCurrentStatuses, +} from '@selectors/statuses'; -const getSubmissionsFetchingState = state => state.submissions.fetchingState; +const getSubmissions = state => state.submissions.byID; const getCurrentSubmissionID = state => state.submissions.current; -const getByGivenStatusesObject = statuses => createSelector( - [getSubmissionsByStatuses], (byStatuses) => { - return statuses.reduce((acc, status) => acc.concat(byStatuses[status] || []), []) - } -); - -const getSubmissionsByGivenStatuses = statuses => createSelector( - [getSubmissions, getByGivenStatusesObject(statuses)], - (submissions, byStatus) => byStatus.map(id => submissions[id]) -); -const getByGivenStatusesError = statuses => createSelector( - [getSubmissionsFetchingState], - state => state.isErrored === true +const getCurrentRoundSubmissions = createSelector( + [ getCurrentRoundSubmissionIDs, getSubmissions], + (submissionIDs, submissions) => { + return submissionIDs.map(submissionID => submissions[submissionID]); + } ); -const getByGivenStatusesLoading = statuses => createSelector( - [getByGivenStatusesObject], - state => state.isFetching === true -); -const getCurrentRoundSubmissions = createSelector( - [ getCurrentRoundSubmissionIDs, getSubmissions], +const getCurrentStatusesSubmissions = createSelector( + [ getSubmissionIDsForCurrentStatuses, getSubmissions], (submissionIDs, submissions) => { return submissionIDs.map(submissionID => submissions[submissionID]); } @@ -64,19 +49,13 @@ const getSubmissionsByRoundError = state => state.rounds.error; const getSubmissionsByRoundLoadingState = state => state.submissions.itemsLoading === true; export { - getByGivenStatusesError, - getByGivenStatusesLoading, - getCurrentRoundID, - getCurrentRound, - getCurrentRoundSubmissionIDs, getCurrentRoundSubmissions, getCurrentSubmission, getCurrentSubmissionID, - getRounds, getSubmissionsByRoundError, getSubmissionsByRoundLoadingState, getSubmissionLoadingState, getSubmissionErrorState, getSubmissionOfID, - getSubmissionsByGivenStatuses, + getCurrentStatusesSubmissions, }; diff --git a/opentech/static_src/src/app/src/redux/store.js b/opentech/static_src/src/app/src/redux/store.js index a7002461574d3319ee31b524868b725189d11b00..17b69824b8a1d025d5568cc01bd34eb46bc3eb2f 100644 --- a/opentech/static_src/src/app/src/redux/store.js +++ b/opentech/static_src/src/app/src/redux/store.js @@ -2,11 +2,17 @@ import { createStore, applyMiddleware } from 'redux' import ReduxThunk from 'redux-thunk' import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly' import logger from 'redux-logger' +import { routerMiddleware } from 'connected-react-router'; +import { createBrowserHistory } from 'history'; -import rootReducer from '@reducers'; +import createRootReducer from '@reducers'; import api from '@middleware/api' + +export const history = createBrowserHistory(); + const MIDDLEWARE = [ + routerMiddleware(history), ReduxThunk, api, ]; @@ -18,7 +24,8 @@ if (process.env.NODE_ENV === 'development') { export default initialState => { const store = createStore( - rootReducer, + createRootReducer(history), + initialState, composeWithDevTools( applyMiddleware(...MIDDLEWARE) ) diff --git a/opentech/static_src/src/app/src/submissionsByRoundIndex.js b/opentech/static_src/src/app/src/submissionsByRoundIndex.js index cf189d8c154a5e39ec2d5bb39cca8b05a27f7fdf..c7ee2c9319b94de694cbb67cf45741b846b07a0b 100644 --- a/opentech/static_src/src/app/src/submissionsByRoundIndex.js +++ b/opentech/static_src/src/app/src/submissionsByRoundIndex.js @@ -2,9 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Modal from 'react-modal'; import { Provider } from 'react-redux'; +import { ConnectedRouter } from 'connected-react-router'; import SubmissionsByRoundApp from './SubmissionsByRoundApp'; -import createStore from '@redux/store'; +import createStore, { history } from '@redux/store'; const container = document.getElementById('submissions-by-round-react-app'); @@ -15,7 +16,9 @@ Modal.setAppElement(container) ReactDOM.render( <Provider store={store}> - <SubmissionsByRoundApp pageContent={container.innerHTML} roundID={parseInt(container.dataset.roundId)} /> + <ConnectedRouter history={history}> + <SubmissionsByRoundApp pageContent={container.innerHTML} roundID={parseInt(container.dataset.roundId)} /> + </ConnectedRouter> </Provider>, container ); diff --git a/opentech/static_src/src/app/src/submissionsByStatusIndex.js b/opentech/static_src/src/app/src/submissionsByStatusIndex.js index 830d011a382c3a4f39b22a90cf71df443786487d..a62ef16ead6ca6082d11da3a8d015a84b7a85cc0 100644 --- a/opentech/static_src/src/app/src/submissionsByStatusIndex.js +++ b/opentech/static_src/src/app/src/submissionsByStatusIndex.js @@ -2,9 +2,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Modal from 'react-modal'; import { Provider } from 'react-redux'; +import { ConnectedRouter } from 'connected-react-router'; import SubmissionsByStatusApp from './SubmissionsByStatusApp'; -import createStore from '@redux/store'; +import createStore, { history } from '@redux/store'; const container = document.getElementById('submissions-by-status-react-app'); @@ -15,7 +16,9 @@ Modal.setAppElement(container) ReactDOM.render( <Provider store={store}> - <SubmissionsByStatusApp pageContent={container.innerHTML} statuses={container.dataset.statuses.split(',')} /> + <ConnectedRouter history={history}> + <SubmissionsByStatusApp pageContent={container.innerHTML} statuses={container.dataset.statuses.split(',')} /> + </ConnectedRouter> </Provider>, container ); diff --git a/opentech/static_src/src/javascript/apply/submission-filters.js b/opentech/static_src/src/javascript/apply/submission-filters.js index 2201fc5d3a2eea39451dedd21ceb1b7d177a5727..c89cea1431f76204072080fa5a5725ced7e4a2a4 100644 --- a/opentech/static_src/src/javascript/apply/submission-filters.js +++ b/opentech/static_src/src/javascript/apply/submission-filters.js @@ -18,7 +18,7 @@ const urlParams = new URLSearchParams(window.location.search); - const persistedParams = ['sort', 'query']; + const persistedParams = ['sort', 'query', 'submission']; // check if the page has a query string and keep filters open if so on desktop const minimumNumberParams = persistedParams.reduce( diff --git a/package-lock.json b/package-lock.json index 558039534c538dfdd9f6d0fcd1b9b2d44ecfe3b5..7d30996318d42ab39c0e257f4a7244b65c830b46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2473,6 +2473,22 @@ "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, + "connected-react-router": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.3.1.tgz", + "integrity": "sha512-nhuQiLOAQlCgkCypGSUhycgaqqTh2IUwVFvzw2y13v8JqB92yTk3yeAKG6X1b0IcD7S4gQizYbjgejf7DJjbyw==", + "requires": { + "immutable": "3.8.2", + "seamless-immutable": "7.1.4" + }, + "dependencies": { + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + } + } + }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -9535,6 +9551,11 @@ } } }, + "seamless-immutable": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz", + "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", diff --git a/package.json b/package.json index 735a7402d2396ad25e5ff038a4479411566e2756..afd1acccf7253bed76bf2568c69a64d8bfd10a22 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@babel/polyfill": "^7.2.5", "@rooks/use-interval": "^1.2.0", "@svgr/webpack": "^4.1.0", + "connected-react-router": "^6.3.1", "del": "^3.0.0", "gulp": "^4.0.0", "gulp-babel": "^8.0.0", @@ -32,6 +33,7 @@ "react-dom": "^16.8.1", "react-modal": "^3.8.1", "react-redux": "^6.0.0", + "react-router-dom": "^4.3.1", "react-rte": "^0.16.1", "react-transition-group": "^2.5.3", "react-window-size-listener": "^1.2.3",