diff --git a/opentech/apply/funds/serializers.py b/opentech/apply/funds/serializers.py index bcc1fdc65652149e15edcd7d28b3900e84fec1a1..e96dda318965b354fa90bc585058702cec42a292 100644 --- a/opentech/apply/funds/serializers.py +++ b/opentech/apply/funds/serializers.py @@ -28,7 +28,7 @@ class SubmissionListSerializer(serializers.ModelSerializer): class Meta: model = ApplicationSubmission - fields = ('id', 'title', 'status', 'url') + fields = ('id', 'title', 'status', 'url', 'round') class SubmissionDetailSerializer(serializers.ModelSerializer): diff --git a/opentech/apply/funds/templates/funds/submissions_by_status.html b/opentech/apply/funds/templates/funds/submissions_by_status.html index 7b8f1a2425065fef150f559707e3eb62d61bb0d3..0cbd08c1b79098e8eadf0a415531e4fb00609490 100644 --- a/opentech/apply/funds/templates/funds/submissions_by_status.html +++ b/opentech/apply/funds/templates/funds/submissions_by_status.html @@ -13,7 +13,7 @@ </div> </div> - <div id="submissions-by-status-react-app" data-statuses="{{ statuses }}"> + <div id="submissions-by-status-react-app" data-statuses="{{ statuses|join:',' }}"> <div class="wrapper wrapper--large wrapper--inner-space-medium"> {% block table %} {{ block.super }} diff --git a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js index 133ec908ac6d5d229408761c8f6bb708eae82fed..1d09452522140562ba6875ba05c7dcf2c251298e 100644 --- a/opentech/static_src/src/app/src/SubmissionsByRoundApp.js +++ b/opentech/static_src/src/app/src/SubmissionsByRoundApp.js @@ -48,7 +48,7 @@ class SubmissionsByRoundApp extends React.Component { <div style={this.state.style} ref={this.setOriginalContentRef} dangerouslySetInnerHTML={{ __html: this.props.pageContent }} /> {this.state.detailOpened && - <GroupByStatusDetailView roundId={this.props.roundID} /> + <GroupByStatusDetailView /> } </> ) diff --git a/opentech/static_src/src/app/src/SubmissionsByStatusApp.js b/opentech/static_src/src/app/src/SubmissionsByStatusApp.js index d2c153b1c8e15d9aac43cf27ec7226468a108c0a..57c62ec45908d2142eceebf00d931d068f8fec8b 100644 --- a/opentech/static_src/src/app/src/SubmissionsByStatusApp.js +++ b/opentech/static_src/src/app/src/SubmissionsByStatusApp.js @@ -3,22 +3,19 @@ import PropTypes from 'prop-types'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux' +import GroupByRoundDetailView from '@containers/GroupByRoundDetailView'; import Switcher from '@components/Switcher'; class SubmissionsByStatusApp extends React.Component { static propTypes = { - roundID: PropTypes.number, - setSubmissionRound: PropTypes.func, pageContent: PropTypes.node.isRequired, + statuses: PropTypes.arrayOf(PropTypes.string), }; state = { detailOpened: false }; - componentDidMount() { - } - openDetail = () => { this.setState(state => ({ style: { ...state.style, display: 'none' } , @@ -45,8 +42,7 @@ class SubmissionsByStatusApp extends React.Component { <div style={this.state.style} ref={this.setOriginalContentRef} dangerouslySetInnerHTML={{ __html: this.props.pageContent }} /> {this.state.detailOpened && - //<GroupByStatusDetailView roundId={this.props.roundID} /> - <p>Test</p> + <GroupByRoundDetailView submissionStatuses={this.props.statuses} /> } </> ) diff --git a/opentech/static_src/src/app/src/api/index.js b/opentech/static_src/src/app/src/api/index.js index f670f1ba6ce1bf9e4852781be5afe7b286da9a5e..39fbfe7c11d8a35a658bd9f11ecf59a1b8ff1d1e 100644 --- a/opentech/static_src/src/app/src/api/index.js +++ b/opentech/static_src/src/app/src/api/index.js @@ -1,9 +1,10 @@ -import { fetchSubmission, fetchSubmissionsByRound } from '@api/submissions'; +import { fetchSubmission, fetchSubmissionsByRound, fetchSubmissionsByStatuses } from '@api/submissions'; import { fetchRound } from '@api/rounds'; import { createNoteForSubmission, fetchNotesForSubmission } from '@api/notes'; export default { fetchSubmissionsByRound, + fetchSubmissionsByStatuses, fetchSubmission, fetchRound, diff --git a/opentech/static_src/src/app/src/api/submissions.js b/opentech/static_src/src/app/src/api/submissions.js index a1505ebf50ce314aded651753c7abfc4f2285f61..f6f2cb41fc9acfcfe364cf07cdac9828ff7cbbb7 100644 --- a/opentech/static_src/src/app/src/api/submissions.js +++ b/opentech/static_src/src/app/src/api/submissions.js @@ -14,3 +14,14 @@ export function fetchSubmission(id) { path: `/apply/api/submissions/${id}/`, }; } + +export function fetchSubmissionsByStatuses(statuses) { + const params = new URLSearchParams + params.append('page_size', 1000) + statuses.forEach(v => params.append('status', v)); + + return { + path:'/apply/api/submissions/', + params, + }; +} diff --git a/opentech/static_src/src/app/src/api/utils.js b/opentech/static_src/src/app/src/api/utils.js index ed1c123f417b39a540f2f157cd0bcbdbbe4505d1..29e4a3ef537aaa4054ba4738a0eb1fb989c288a8 100644 --- a/opentech/static_src/src/app/src/api/utils.js +++ b/opentech/static_src/src/app/src/api/utils.js @@ -4,14 +4,12 @@ const getBaseUrl = () => { return process.env.API_BASE_URL; }; -export function apiFetch({path, method = 'GET', params = {}, options = {}}) { +export function apiFetch({path, method = 'GET', params = new URLSearchParams, options = {}}) { const url = new URL(getBaseUrl()); url.pathname = path; - if (params !== undefined) { - for (const [paramKey, paramValue] of Object.entries(params)) { - url.searchParams.set(paramKey, paramValue); - } + for (const [paramKey, paramValue] of getIteratorForParams(params)) { + url.searchParams.append(paramKey, paramValue); } if (['post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) { @@ -37,3 +35,12 @@ export function apiFetch({path, method = 'GET', params = {}, options = {}}) { function getCSRFToken() { return Cookies.get('csrftoken'); } + + +function getIteratorForParams(params) { + if (params instanceof URLSearchParams) { + return params; + } + + return Object.entries(params); +} 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 225bfcf4cdbf9551ca099e24f3428fc8025379ac..4918a4a3ca14d4c0b0f54c056dd41445c9a6ae2c 100644 --- a/opentech/static_src/src/app/src/components/GroupedListing/index.js +++ b/opentech/static_src/src/app/src/components/GroupedListing/index.js @@ -89,7 +89,6 @@ export default class GroupedListing extends React.Component { orderItems() { const groupedItems = this.getGroupedItems(); const { order = [] } = this.props; - const orderedItems = order.map(({key, display, values}) => ({ name: display, key, diff --git a/opentech/static_src/src/app/src/containers/ByRoundListing.js b/opentech/static_src/src/app/src/containers/ByRoundListing.js new file mode 100644 index 0000000000000000000000000000000000000000..5b0c9b9416d805228f0d630e13df245cf6226326 --- /dev/null +++ b/opentech/static_src/src/app/src/containers/ByRoundListing.js @@ -0,0 +1,91 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux' + +import GroupedListing from '@components/GroupedListing'; +import { + loadSubmissionsOfStatuses, + setCurrentSubmission, +} from '@actions/submissions'; +import { + getSubmissionsByGivenStatuses, + getCurrentSubmissionID, + getSubmissionsByRoundError, +} from '@selectors/submissions'; + + +const loadData = props => { + props.loadSubmissions() +} + +class ByRoundListing extends React.Component { + static propTypes = { + submissionStatuses: PropTypes.arrayOf(PropTypes.string), + loadSubmissions: PropTypes.func, + submissions: PropTypes.arrayOf(PropTypes.object), + error: PropTypes.string, + setCurrentItem: PropTypes.func, + activeSubmission: PropTypes.number, + shouldSelectFirst: PropTypes.bool, + }; + + componentDidMount() { + // Update items if round ID is defined. + if ( this.props.submissionStatuses ) { + loadData(this.props) + } + } + + componentDidUpdate(prevProps) { + const { submissionStatuses } = this.props; + // Update entries if round ID is changed or is not null. + if (!submissionStatuses.every(v => prevProps.submissionStatuses.includes(v))) { + loadData(this.props) + } + } + + + prepareOrder = () => { + const rounds = this.props.submissions + .map(v => v.round) + .filter((value, index, arr) => arr.indexOf(value) === index); + return rounds.map((v, i) => ({ + display: `Round ${v}`, + key: v, + position: i, + values: [v], + })); + } + + render() { + const { error, submissions, setCurrentItem, activeSubmission, shouldSelectFirst} = this.props; + const isLoading = false + const order = this.prepareOrder(); + return <GroupedListing + isLoading={isLoading} + error={error} + items={submissions} + activeItem={activeSubmission} + onItemSelection={setCurrentItem} + shouldSelectFirst={shouldSelectFirst} + groupBy={'round'} + order={order} + />; + } +} + +const mapStateToProps = (state, ownProps) => ({ + submissions: getSubmissionsByGivenStatuses(ownProps.submissionStatuses)(state), + error: getSubmissionsByRoundError(state), + activeSubmission: getCurrentSubmissionID(state), +}) + +const mapDispatchToProps = (dispatch, ownProps) => ({ + loadSubmissions: () => dispatch(loadSubmissionsOfStatuses(ownProps.submissionStatuses)), + setCurrentItem: id => dispatch(setCurrentSubmission(id)), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ByRoundListing); diff --git a/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js new file mode 100644 index 0000000000000000000000000000000000000000..657a6f871220600cce0727cdf9f1733d6fb71368 --- /dev/null +++ b/opentech/static_src/src/app/src/containers/GroupByRoundDetailView.js @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import DetailView from '@components/DetailView'; +import ByRoundListing from '@containers/ByRoundListing'; + +export default class GroupByRoundDetailView extends React.Component { + static propTypes = { + submissionStatuses: PropTypes.arrayOf(PropTypes.string), + }; + + render() { + const listing = <ByRoundListing submissionStatuses={this.props.submissionStatuses} />; + return ( + <DetailView listing={listing} /> + ); + } +} 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 11ff549a44b1f7066e1baa303c59aa8287d46871..0c9afca1ec100f6a6cb0a4c285b6581f0bebbdcf 100644 --- a/opentech/static_src/src/app/src/redux/actions/submissions.js +++ b/opentech/static_src/src/app/src/redux/actions/submissions.js @@ -21,6 +21,11 @@ export const UPDATE_SUBMISSIONS_BY_ROUND = 'UPDATE_SUBMISSIONS_BY_ROUND'; export const START_LOADING_SUBMISSIONS_BY_ROUND = 'START_LOADING_SUBMISSIONS_BY_ROUND'; 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'; + // Submissions export const SET_CURRENT_SUBMISSION = 'SET_CURRENT_SUBMISSION'; export const START_LOADING_SUBMISSION = 'START_LOADING_SUBMISSION'; @@ -81,6 +86,32 @@ const fetchSubmissionsByRound = (roundID) => ({ roundID, }) + +const fetchSubmissionsByStatuses = statuses => { + if(!Array.isArray(statuses)) { + throw new Error("Statuses have to be an array of statuses"); + } + + return { + [CALL_API]: { + types: [ START_LOADING_SUBMISSIONS_BY_STATUSES, UPDATE_SUBMISSIONS_BY_STATUSES, FAIL_LOADING_SUBMISSIONS_BY_STATUSES], + endpoint: api.fetchSubmissionsByStatuses(statuses), + }, + statuses, + }; +}; + +export const loadSubmissionsOfStatuses = statuses => (dispatch, getState) => { + //const state = getState() + //const submissions = getCurrentRoundSubmissionIDs(state) + + //if ( submissions && submissions.length !== 0 ) { + //return null + //} + + return dispatch(fetchSubmissionsByStatuses(statuses)) +} + const fetchSubmission = (submissionID) => ({ [CALL_API]: { types: [ START_LOADING_SUBMISSION, UPDATE_SUBMISSION, FAIL_LOADING_SUBMISSION], 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 6e66e225a39375232849ec2610b9a4a33b9dd6a3..d93d1274635ceced1d7aa21b8096a08b1ca5b75b 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, + UPDATE_SUBMISSIONS_BY_STATUSES, } from '@actions/submissions'; import { UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes' @@ -63,6 +64,7 @@ function submissionsByID(state = {}, action) { ...state, [action.submissionID]: submission(state[action.submissionID], action), }; + case UPDATE_SUBMISSIONS_BY_STATUSES: case UPDATE_SUBMISSIONS_BY_ROUND: return { ...state, @@ -95,9 +97,23 @@ function currentSubmission(state = null, action) { } +function submissionsByStatuses(state = {}, action) { + switch (action.type) { + case UPDATE_SUBMISSIONS_BY_STATUSES: + return { + ...state, + [action.statuses]: action.data.results.map(v => v.id), + }; + default: + return state + } +} + + const submissions = combineReducers({ byID: submissionsByID, current: currentSubmission, + byStatuses: submissionsByStatuses, }); export default submissions; 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 d00a80dd8e99d05421a121221ab95e2b4ce5cdc9..5946449369a65c7fb076998eeb2beed68860bb6b 100644 --- a/opentech/static_src/src/app/src/redux/selectors/submissions.js +++ b/opentech/static_src/src/app/src/redux/selectors/submissions.js @@ -4,6 +4,8 @@ const getSubmissions = state => state.submissions.byID; const getRounds = state => state.rounds.byID; +const getSubmissionsByStatuses = state => state.submissions.byStatuses; + const getCurrentRoundID = state => state.rounds.current; const getCurrentRound = createSelector( @@ -15,6 +17,18 @@ const getCurrentRound = createSelector( const getCurrentSubmissionID = state => state.submissions.current; +const getSubmissionsByGivenStatuses = statuses => createSelector( + [getSubmissions, getSubmissionsByStatuses], (submissions, byStatuses) => { + for (const [key, value] of Object.entries(byStatuses)) { + if (key.split(',').every(v => statuses.includes(v))) { + return value.map(id => submissions[id]) + } + } + + return [] + } +); + const getCurrentRoundSubmissionIDs = createSelector( [ getCurrentRound ], (round) => { @@ -61,4 +75,5 @@ export { getSubmissionLoadingState, getSubmissionErrorState, getSubmissionOfID, + getSubmissionsByGivenStatuses, }; diff --git a/opentech/static_src/src/app/src/submissionsByStatusIndex.js b/opentech/static_src/src/app/src/submissionsByStatusIndex.js index 317ce892885238f413dfa76b3724a5205f3c7d0a..e492499ee6a10917cf0f45a021908941ed5f752a 100644 --- a/opentech/static_src/src/app/src/submissionsByStatusIndex.js +++ b/opentech/static_src/src/app/src/submissionsByStatusIndex.js @@ -12,7 +12,7 @@ const store = createStore(); ReactDOM.render( <Provider store={store}> - <SubmissionsByStatusApp pageContent={container.innerHTML} /> + <SubmissionsByStatusApp pageContent={container.innerHTML} statuses={container.dataset.statuses.split(',')} /> </Provider>, container ); diff --git a/opentech/static_src/src/app/webpack.base.config.js b/opentech/static_src/src/app/webpack.base.config.js index 0cdcd31ef072711fc46a0b7572f7245268547fa2..9942fe66aa894b2721a11ed82524f93bac79f9d1 100644 --- a/opentech/static_src/src/app/webpack.base.config.js +++ b/opentech/static_src/src/app/webpack.base.config.js @@ -9,6 +9,7 @@ module.exports = { submissionsByRound: COMMON_ENTRY.concat(['./src/submissionsByRoundIndex']), submissionsByStatus: COMMON_ENTRY.concat(['./src/submissionsByStatusIndex']), }, + output: { filename: '[name]-[hash].js' }, diff --git a/package-lock.json b/package-lock.json index 6553c6995b6f055d9adc352016f2535403329d69..f386d671c721f9b990c2c2aa01f46913f548e3b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4389,7 +4389,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4407,11 +4408,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4424,15 +4427,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4535,7 +4541,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4545,6 +4552,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4557,17 +4565,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4584,6 +4595,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4656,7 +4668,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4666,6 +4679,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4741,7 +4755,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4771,6 +4786,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4788,6 +4804,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4826,11 +4843,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } },