From b2e9899c7193786893f0c4c6cc15724e0acbf7d3 Mon Sep 17 00:00:00 2001 From: vimal1083 <vimal1083@gmail.com> Date: Tue, 5 Jan 2021 12:24:52 +0530 Subject: [PATCH] Add Filters in react app --- hypha/apply/api/v1/filters.py | 21 ++- hypha/apply/api/v1/urls.py | 2 + hypha/apply/api/v1/views.py | 48 ++++++ .../src/app/src/AllSubmissionsApp.js | 13 +- .../src/app/src/SubmissionsByRoundApp.js | 8 +- .../src/app/src/SubmissionsByStatusApp.js | 11 +- hypha/static_src/src/app/src/SwitcherApp.js | 5 +- hypha/static_src/src/app/src/api/index.js | 2 +- .../static_src/src/app/src/api/submissions.js | 43 ++++- .../common/components/FilterDropDown/index.js | 67 ++++++++ .../src/components/GroupedListing/index.js | 13 +- .../src/components/GroupedListing/styles.scss | 1 + .../src/app/src/containers/ByRoundListing.js | 10 +- .../src/app/src/containers/ByStatusListing.js | 5 +- .../src/containers/ScreeningStatus/sagas.js | 11 +- .../containers/SubmissionFilters/actions.js | 33 ++++ .../containers/SubmissionFilters/constants.js | 8 + .../src/containers/SubmissionFilters/index.js | 161 ++++++++++++++++++ .../containers/SubmissionFilters/models.js | 10 ++ .../containers/SubmissionFilters/reducer.js | 28 +++ .../src/containers/SubmissionFilters/sagas.js | 26 +++ .../containers/SubmissionFilters/selectors.js | 9 + .../containers/SubmissionFilters/styles.scss | 14 ++ .../src/app/src/redux/actions/submissions.js | 75 ++++---- .../src/app/src/redux/reducers/rounds.js | 4 + .../src/app/src/redux/reducers/statuses.js | 3 + .../src/app/src/redux/reducers/submissions.js | 6 +- .../src/app/src/redux/selectors/statuses.js | 1 + .../app/src/redux/selectors/submissions.js | 7 + 29 files changed, 577 insertions(+), 68 deletions(-) create mode 100644 hypha/static_src/src/app/src/common/components/FilterDropDown/index.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/actions.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/constants.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/index.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/models.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/reducer.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/sagas.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/selectors.js create mode 100644 hypha/static_src/src/app/src/containers/SubmissionFilters/styles.scss diff --git a/hypha/apply/api/v1/filters.py b/hypha/apply/api/v1/filters.py index 1c915c6ac..4613bc17d 100644 --- a/hypha/apply/api/v1/filters.py +++ b/hypha/apply/api/v1/filters.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from django.db.models import Q from django_filters import rest_framework as filters from wagtail.core.models import Page @@ -8,6 +9,7 @@ from hypha.apply.funds.models import ( FundType, LabType, RoundsAndLabs, + ScreeningStatus, ) from hypha.apply.funds.workflow import PHASES @@ -29,10 +31,23 @@ class SubmissionsFilter(filters.FilterSet): field_name='page', label='fund', queryset=Page.objects.type(FundType) | Page.objects.type(LabType) ) + screening_statuses = filters.ModelMultipleChoiceFilter( + field_name='screening_statuses', + queryset=ScreeningStatus.objects.all(), + ) + reviewers = filters.ModelMultipleChoiceFilter( + field_name='reviewers', + queryset=get_user_model().objects.all(), + ) + lead = filters.ModelMultipleChoiceFilter( + field_name='lead', + queryset=get_user_model().objects.all(), + ) - class Meta: - model = ApplicationSubmission - fields = ('status', 'round', 'active', 'submit_date', 'fund', ) + +class Meta: + model = ApplicationSubmission + fields = ('status', 'round', 'active', 'submit_date', 'fund', 'screening_statuses', 'reviewers', 'lead') def filter_active(self, qs, name, value): if value is None: diff --git a/hypha/apply/api/v1/urls.py b/hypha/apply/api/v1/urls.py index 8a38f23af..6d990a562 100644 --- a/hypha/apply/api/v1/urls.py +++ b/hypha/apply/api/v1/urls.py @@ -14,6 +14,7 @@ from .views import ( RoundViewSet, SubmissionActionViewSet, SubmissionCommentViewSet, + SubmissionFilters, SubmissionViewSet, ) @@ -35,6 +36,7 @@ submission_router.register(r'screening_statuses', SubmissionScreeningStatusViewS urlpatterns = [ path('user/', CurrentUser.as_view(), name='user'), + path('submissions_filter/', SubmissionFilters.as_view(), name='submissions-filter') ] urlpatterns = router.urls + submission_router.urls + urlpatterns diff --git a/hypha/apply/api/v1/views.py b/hypha/apply/api/v1/views.py index a89b6687a..0682fc87b 100644 --- a/hypha/apply/api/v1/views.py +++ b/hypha/apply/api/v1/views.py @@ -14,6 +14,8 @@ from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.activity.models import COMMENT, Activity from hypha.apply.determinations.views import DeterminationCreateOrUpdateView from hypha.apply.funds.models import ApplicationSubmission, RoundsAndLabs +from hypha.apply.funds.tables import get_reviewers +from hypha.apply.funds.workflow import STATUSES from hypha.apply.review.models import Review from .filters import CommentFilter, SubmissionsFilter @@ -54,6 +56,52 @@ class SubmissionViewSet(viewsets.ReadOnlyModelViewSet): ) +class SubmissionFilters(APIView): + permission_classes = [permissions.IsAuthenticated] + + def filter_unique_options(self, options): + unique_items = [dict(item) for item in {tuple(option.items()) for option in options}] + return list(filter(lambda x: len(x.get("label")), unique_items)) + + def format(self, filterKey, label, options): + return { + "filterKey": filterKey, + "label": label, + "options": options + } + + def get(self, request, format=None): + submissions = ApplicationSubmission.objects.for_table(user=self.request.user) + filter_options = [ + self.format("fund", "Funds", [ + {"key": submission.page.id, "label": submission.page.title} + for submission in submissions.distinct('page') + ]), + self.format("round", "Rounds", [ + {"key": submission.round.id, "label": submission.round.title} + for submission in submissions.distinct('round').exclude(round__isnull=True) + ]), + self.format("status", "Statuses", [ + {'key': list(STATUSES.get(label)), 'label': label} + for label in dict(STATUSES) + ]), + self.format("screening_statuses", "Screenings", self.filter_unique_options([ + {"key": screening.get("id"), "label": screening.get("title")} + for submission in submissions.distinct('screening_statuses') + for screening in submission.screening_statuses.values() + ])), + self.format("lead", "Leads", [ + {"key": submission.lead.id, "label": submission.lead.full_name} + for submission in submissions.distinct('lead') + ]), + self.format("reviewers", "Reviewers", self.filter_unique_options([ + {"key": reviewer.get('id'), "label": reviewer.get('full_name') or reviewer.get('email')} + for reviewer in get_reviewers(request).values() + ])), + ] + return Response(filter_options) + + class SubmissionActionViewSet( SubmissionNestedMixin, viewsets.GenericViewSet diff --git a/hypha/static_src/src/app/src/AllSubmissionsApp.js b/hypha/static_src/src/app/src/AllSubmissionsApp.js index 29eedf97b..f2c53cebd 100644 --- a/hypha/static_src/src/app/src/AllSubmissionsApp.js +++ b/hypha/static_src/src/app/src/AllSubmissionsApp.js @@ -13,7 +13,8 @@ class AllSubmissionsApp extends React.Component { static propTypes = { pageContent: PropTypes.node.isRequired, setStatuses: PropTypes.func.isRequired, - submissions: PropTypes.array + submissions: PropTypes.array, + doNotRenderFilter: PropTypes.array }; @@ -21,12 +22,18 @@ class AllSubmissionsApp extends React.Component { this.props.setStatuses([]); } + onfilter = () => { + this.props.setStatuses([]) + } + render() { return ( <SwitcherApp - detailComponent={<GroupByRoundDetailView submissions= {this.props.submissions} groupBy = "all"/>} + detailComponent={<GroupByRoundDetailView submissions= {this.props.submissions} groupBy="all"/>} switcherSelector={'submissions-all-react-app-switcher'} - pageContent={this.props.pageContent} /> + doNotRenderFilter={[]} + pageContent={this.props.pageContent} + onFilter={this.onfilter} /> ) } } diff --git a/hypha/static_src/src/app/src/SubmissionsByRoundApp.js b/hypha/static_src/src/app/src/SubmissionsByRoundApp.js index 26cdc348f..fff434a25 100644 --- a/hypha/static_src/src/app/src/SubmissionsByRoundApp.js +++ b/hypha/static_src/src/app/src/SubmissionsByRoundApp.js @@ -19,12 +19,18 @@ class SubmissionsByRoundApp extends React.Component { this.props.setSubmissionRound(this.props.roundID); } + onfilter = () => { + this.props.setSubmissionRound(this.props.roundID); + } + render() { return ( <SwitcherApp detailComponent={<GroupByStatusDetailView />} switcherSelector={'submissions-by-round-app-react-switcher'} - pageContent={this.props.pageContent} /> + pageContent={this.props.pageContent} + doNotRenderFilter={['round', 'fund', 'lead']} + onFilter={this.onfilter} /> ) } } diff --git a/hypha/static_src/src/app/src/SubmissionsByStatusApp.js b/hypha/static_src/src/app/src/SubmissionsByStatusApp.js index 3b9325b67..e2f757cd0 100644 --- a/hypha/static_src/src/app/src/SubmissionsByStatusApp.js +++ b/hypha/static_src/src/app/src/SubmissionsByStatusApp.js @@ -21,11 +21,17 @@ class SubmissionsByStatusApp extends React.Component { this.props.setStatuses(this.props.statuses); } + onfilter = () => { + this.props.setStatuses(this.props.statuses); + } + render() { return <SwitcherApp detailComponent={<GroupByRoundDetailView submissions= {this.props.submissions}/>} switcherSelector={'submissions-by-status-app-react-switcher'} - pageContent={this.props.pageContent} />; + pageContent={this.props.pageContent} + doNotRenderFilter={['status']} + onFilter={this.onfilter} />; } } @@ -35,7 +41,8 @@ const mapStateToProps = (state, ownProps) => ({ const mapDispatchToProps = dispatch => { return { - setStatuses: statuses => {dispatch(setCurrentStatuses(statuses)); + setStatuses: (statuses) => { + dispatch(setCurrentStatuses(statuses)); }, } }; diff --git a/hypha/static_src/src/app/src/SwitcherApp.js b/hypha/static_src/src/app/src/SwitcherApp.js index 20f58497d..c28f41034 100644 --- a/hypha/static_src/src/app/src/SwitcherApp.js +++ b/hypha/static_src/src/app/src/SwitcherApp.js @@ -10,6 +10,7 @@ import { setCurrentSubmissionParam, } from '@actions/submissions'; import GeneralInfoContainer from '@containers/GeneralInfo' +import SubmissionFiltersContainer from '@containers/SubmissionFilters' class SwitcherApp extends React.Component { @@ -22,6 +23,8 @@ class SwitcherApp extends React.Component { searchParam: PropTypes.string, setParams: PropTypes.func.isRequired, clearParams: PropTypes.func.isRequired, + onFilter: PropTypes.func, + doNotRenderFilter: PropTypes.array }; state = { @@ -91,7 +94,7 @@ class SwitcherApp extends React.Component { <Switcher selector={this.props.switcherSelector} open={this.state.detailOpened} handleOpen={this.openDetail} handleClose={this.closeDetail} /> <div style={this.state.style} ref={this.setOriginalContentRef} dangerouslySetInnerHTML={{ __html: this.props.pageContent }} /> - + {this.state.detailOpened && <SubmissionFiltersContainer onFilter={this.props.onFilter} doNotRender={this.props.doNotRenderFilter}/>} {this.state.detailOpened && this.props.detailComponent} </> ) diff --git a/hypha/static_src/src/app/src/api/index.js b/hypha/static_src/src/app/src/api/index.js index 320423098..f9939fea7 100644 --- a/hypha/static_src/src/app/src/api/index.js +++ b/hypha/static_src/src/app/src/api/index.js @@ -4,7 +4,7 @@ import { fetchSubmissionsByRound, fetchSubmissionsByStatuses, fetchReviewDraft, - fetchDeterminationDraft + fetchDeterminationDraft, } from '@api/submissions'; import { fetchRound, fetchRounds } from '@api/rounds'; import { createNoteForSubmission, fetchNotesForSubmission, fetchNewNotesForSubmission, editNoteForSubmission } from '@api/notes'; diff --git a/hypha/static_src/src/app/src/api/submissions.js b/hypha/static_src/src/app/src/api/submissions.js index fc51f4d01..257fa6d7c 100644 --- a/hypha/static_src/src/app/src/api/submissions.js +++ b/hypha/static_src/src/app/src/api/submissions.js @@ -1,10 +1,25 @@ -export function fetchSubmissionsByRound(id) { +export function fetchSubmissionsByRound(id, filters) { + const params = new URLSearchParams + params.append('page_size', 1000) + params.append('round', id) + + if(filters){ + filters.forEach(filter => + { + if(filter.key == 'status'){ + filter.value.map(values => + values.map(value => + params.append(filter.key, value))) + }else{ + filter.value.forEach(filterValue => + params.append(filter.key,filterValue)) + } + } + ) + } return { path:'/v1/submissions/', - params: { - round: id, - page_size: 1000, - } + params }; } @@ -26,11 +41,25 @@ export function fetchDeterminationDraft(id) { }; } -export function fetchSubmissionsByStatuses(statuses) { +export function fetchSubmissionsByStatuses(statuses, filters) { const params = new URLSearchParams params.append('page_size', 1000) statuses.forEach(v => params.append('status', v)); - + + if(filters){ + filters.forEach(filter => + { + if(filter.key == 'status'){ + filter.value.map(values => + values.map(value => + params.append(filter.key, value))) + }else{ + filter.value.forEach(filterValue => + params.append(filter.key,filterValue)) + } + } + ) + } return { path:'/v1/submissions/', params, diff --git a/hypha/static_src/src/app/src/common/components/FilterDropDown/index.js b/hypha/static_src/src/app/src/common/components/FilterDropDown/index.js new file mode 100644 index 000000000..aaa2df1d3 --- /dev/null +++ b/hypha/static_src/src/app/src/common/components/FilterDropDown/index.js @@ -0,0 +1,67 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +// import "./styles.scss"; +import Select from '@material-ui/core/Select'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import FormControl from '@material-ui/core/FormControl'; +import { withStyles } from '@material-ui/core/styles'; +import Checkbox from '@material-ui/core/Checkbox'; +import ListItemText from '@material-ui/core/ListItemText'; +import Input from '@material-ui/core/Input'; + +const styles = { + formControl:{ + minWidth: 200 , + maxWidth: 200, + marginRight: 10, + height: 40 + }, +}; + +class FilterDropDown extends React.PureComponent { + + render() { + const { filter, value, handleChange, renderValues, classes } = this.props; + return <FormControl + variant="outlined" + key={filter.label} + size={"small"} + classes={{ root : classes.formControl }} + > + <InputLabel>{filter.label}</InputLabel> + <Select + multiple + name={filter.filterKey} + value={value} + onChange={handleChange} + input={<Input />} + renderValue={(selected) => renderValues(selected, filter)} + > + {filter["options"].map( + option => <MenuItem + value={option.key} + key={option.key} + > + <Checkbox + checked={value.indexOf(option.key) > -1} + style ={{ color: "#0c72a0b3" }} + /> + <ListItemText primary={option.label} /> + </MenuItem> + )} + </Select> + </FormControl> + } +} + + +FilterDropDown.propTypes = { + filter: PropTypes.object, + value: PropTypes.array, + handleChange: PropTypes.func, + renderValues: PropTypes.func, + classes: PropTypes.object +} + +export default withStyles(styles)(FilterDropDown); diff --git a/hypha/static_src/src/app/src/components/GroupedListing/index.js b/hypha/static_src/src/app/src/components/GroupedListing/index.js index 76e2ca23c..1674b030c 100644 --- a/hypha/static_src/src/app/src/components/GroupedListing/index.js +++ b/hypha/static_src/src/app/src/components/GroupedListing/index.js @@ -42,7 +42,6 @@ export default class GroupedListing extends React.Component { componentDidMount() { this.orderItems(); - // get the height of the dropdown container this.dropdownContainerHeight = this.dropdownContainer.offsetHeight; } @@ -53,9 +52,8 @@ export default class GroupedListing extends React.Component { this.orderItems(); } - if ( this.props.shouldSelectFirst ){ + if (this.props.shouldSelectFirst && this.props.items.length ){ const newItem = this.props.activeItem - // If we dont have an active item, then get one if ( !newItem ) { const firstGroup = this.state.orderedItems[0] @@ -69,8 +67,8 @@ export default class GroupedListing extends React.Component { getGroupedItems() { const { groupBy, items } = this.props; - return items.reduce((tmpItems, v) => { + const groupByValue = v[groupBy]; if (!(groupByValue in tmpItems)) { tmpItems[groupByValue] = []; @@ -79,7 +77,7 @@ export default class GroupedListing extends React.Component { return tmpItems; }, {}); } - + orderItems() { const groupedItems = this.getGroupedItems(); const { order = [] } = this.props; @@ -110,7 +108,7 @@ export default class GroupedListing extends React.Component { </ListingGroup> ); } - + render() { const { isLoading, isErrored, errorMessage } = this.props; const passProps = { @@ -120,12 +118,11 @@ export default class GroupedListing extends React.Component { errorMessage, isErrored }; - + // set css custom prop to allow scrolling from dropdown to last item in the list if (this.listRef.current) { document.documentElement.style.setProperty('--last-listing-item-height', this.listRef.current.lastElementChild.offsetHeight + 'px'); } - return ( <div className="grouped-listing"> <div className="grouped-listing__dropdown" ref={(ref) => this.dropdownContainer = ref}> diff --git a/hypha/static_src/src/app/src/components/GroupedListing/styles.scss b/hypha/static_src/src/app/src/components/GroupedListing/styles.scss index 225412a6f..428f0c35d 100644 --- a/hypha/static_src/src/app/src/components/GroupedListing/styles.scss +++ b/hypha/static_src/src/app/src/components/GroupedListing/styles.scss @@ -14,6 +14,7 @@ @include submission-list-item; height: $listing-header-height; padding: 20px; + background-color: white; } .loading-panel__icon::after { diff --git a/hypha/static_src/src/app/src/containers/ByRoundListing.js b/hypha/static_src/src/app/src/containers/ByRoundListing.js index 089215829..969c453c4 100644 --- a/hypha/static_src/src/app/src/containers/ByRoundListing.js +++ b/hypha/static_src/src/app/src/containers/ByRoundListing.js @@ -24,6 +24,8 @@ import { getByStatusesError, } from '@selectors/statuses'; +import { SelectSelectedFilters } from '@containers/SubmissionFilters/selectors' + const loadData = props => { props.loadRounds() props.loadSubmissions() @@ -41,17 +43,18 @@ class ByRoundListing extends React.Component { rounds: PropTypes.object, isLoading: PropTypes.bool, errorMessage: PropTypes.string, + filters: PropTypes.array }; componentDidMount() { - if ( this.props.statuses) { + if ( this.props.statuses || this.props.filters) { loadData(this.props) } } componentDidUpdate(prevProps) { const { statuses} = this.props; - if (!statuses.every(v => prevProps.statuses.includes(v))) { + if (!statuses.every(v => prevProps.statuses.includes(v)) || this.props.filters != prevProps.filters) { loadData(this.props) } } @@ -91,7 +94,7 @@ class ByRoundListing extends React.Component { const mapStateToProps = (state, ownProps) => ({ statuses: getCurrentStatuses(state), - submissions: ownProps.groupBy ? getSubmissionsForListing(state) :getCurrentStatusesSubmissions(state), + submissions: ownProps.groupBy ? getSubmissionsForListing(state) : getCurrentStatusesSubmissions(state), isErrored: getRoundsErrored(state) || getByStatusesError(state), isLoading: ( getByStatusesLoading(state) || @@ -99,6 +102,7 @@ const mapStateToProps = (state, ownProps) => ({ ), activeSubmission: getCurrentSubmissionID(state), rounds: getRounds(state), + filters : SelectSelectedFilters(state) }) const mapDispatchToProps = (dispatch) => ({ diff --git a/hypha/static_src/src/app/src/containers/ByStatusListing.js b/hypha/static_src/src/app/src/containers/ByStatusListing.js index 573090c98..55f2f675d 100644 --- a/hypha/static_src/src/app/src/containers/ByStatusListing.js +++ b/hypha/static_src/src/app/src/containers/ByStatusListing.js @@ -17,6 +17,7 @@ import { getCurrentRound, getCurrentRoundID, } from '@selectors/rounds'; +import { SelectSelectedFilters } from '@containers/SubmissionFilters/selectors' const loadData = props => { @@ -35,6 +36,7 @@ class ByStatusListing extends React.Component { setCurrentItem: PropTypes.func, activeSubmission: PropTypes.number, shouldSelectFirst: PropTypes.bool, + filters: PropTypes.array }; componentDidMount() { @@ -47,7 +49,7 @@ class ByStatusListing extends React.Component { componentDidUpdate(prevProps) { const { roundID } = this.props; // Update entries if round ID is changed or is not null. - if (roundID && prevProps.roundID !== roundID) { + if (roundID && prevProps.roundID !== roundID || this.props.filters != prevProps.filters) { loadData(this.props) } } @@ -90,6 +92,7 @@ const mapStateToProps = state => ({ round: getCurrentRound(state), errorMessage: getSubmissionsByRoundError(state), activeSubmission: getCurrentSubmissionID(state), + filters: SelectSelectedFilters(state) }) const mapDispatchToProps = dispatch => ({ diff --git a/hypha/static_src/src/app/src/containers/ScreeningStatus/sagas.js b/hypha/static_src/src/app/src/containers/ScreeningStatus/sagas.js index f8b570a67..6a5c9234c 100644 --- a/hypha/static_src/src/app/src/containers/ScreeningStatus/sagas.js +++ b/hypha/static_src/src/app/src/containers/ScreeningStatus/sagas.js @@ -14,15 +14,18 @@ import {select} from 'redux-saga/effects'; function* initialize(action) { try { + if(!action.id) return false; yield put(Actions.showLoadingAction()) let response = yield call(apiFetch, {path : `/v1/screening_statuses/`}); let data = yield response.json() yield put(Actions.getScreeningSuccessAction(data)) response = yield call(apiFetch, {path : `/v1/submissions/${action.id}/screening_statuses/`}) data = yield response.json() + yield put(Actions.setVisibleSelectedAction(data.filter(d => !d.default))) yield put(Actions.setDefaultSelectedAction(data.find(d => d.default) || {})) yield put(Actions.hideLoadingAction()) + } catch (e) { console.log("error", e) yield put(Actions.hideLoadingAction()) @@ -31,6 +34,8 @@ function* initialize(action) { function* setDefaultValue(action){ try{ + if(!action.id) return false; + yield put(Actions.showLoadingAction()) const response = yield call(apiFetch, { @@ -44,6 +49,7 @@ function* setDefaultValue(action){ yield put(Actions.setDefaultSelectedAction(data)) yield put(Actions.setVisibleSelectedAction([])) yield put(Actions.hideLoadingAction()) + }catch(e){ console.log("error", e) yield put(Actions.hideLoadingAction()) @@ -53,6 +59,7 @@ function* setDefaultValue(action){ function* setVisibleOption(action){ try{ + if(!action.id) return false; yield delay(300); yield put(Actions.showLoadingAction()) const screening = yield select(Selectors.selectScreeningInfo) @@ -70,8 +77,8 @@ function* setVisibleOption(action){ const response = yield call(apiFetch, { path : `/v1/submissions/${action.id}/screening_statuses/`, - method : "POST", - options : { + method : "POST", + options : { body : JSON.stringify(updatedData), } }) diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/actions.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/actions.js new file mode 100644 index 000000000..f9e1dacf0 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/actions.js @@ -0,0 +1,33 @@ +import * as ActionTypes from './constants'; + +export const initializeAction = () => ({ + type: ActionTypes.INITIALIZE, +}); + +export const getFiltersSuccessAction = (data) => ({ + type: ActionTypes.GET_FILTERS_SUCCESS, + data +}); + +export const deleteSelectedFiltersAction = () => ({ + type: ActionTypes.DELETE_SELECTED_FILTER +}) + +export const updateFiltersQueryAction = (data) => ({ + type: ActionTypes.UPDATE_FILTERS_QUERY, + data +}); + +export const updateSelectedFilterAction = (filterKey, value) => ({ + type: ActionTypes.UPDATE_SELECTED_FILTER, + filterKey, + value +}); + +export const showLoadingAction = () => ({ + type: ActionTypes.SHOW_LOADING, +}) + +export const hideLoadingAction = () => ({ + type: ActionTypes.HIDE_LOADING, +}) diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/constants.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/constants.js new file mode 100644 index 000000000..6528486c0 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/constants.js @@ -0,0 +1,8 @@ +export const INITIALIZE = 'SubmissionFilters/constants/INITIALIZE'; +export const GET_FILTERS_SUCCESS = 'SubmissionFilters/constants/GET_FILTERS_SUCCESS'; +export const SHOW_LOADING = 'SubmissionFilters/constants/SHOW_LOADING' +export const HIDE_LOADING = 'SubmissionFilters/constants/HIDE_LOADING' +export const SUBMIT_REVIEW_DATA = 'SubmissionFilters/constants/SUBMIT_REVIEW_DATA' +export const UPDATE_SELECTED_FILTER = 'SubmissionFilters/constants/UPDATE_SELECTED_FILTER' +export const UPDATE_FILTERS_QUERY = 'SubmissionFilters/constants/UPDATE_FILTERS_QUERY' +export const DELETE_SELECTED_FILTER = 'SubmissionFilters/constants/DELETE_SELECTED_FILTER' diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/index.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/index.js new file mode 100644 index 000000000..fb966c452 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/index.js @@ -0,0 +1,161 @@ +import React from 'react'; +import injectReducer from '@utils/injectReducer' +import injectSaga from '@utils/injectSaga' +import { withRouter } from 'react-router-dom'; +import { connect } from 'react-redux'; +import { bindActionCreators, compose } from 'redux'; +import PropTypes from 'prop-types'; +import * as Actions from './actions'; +import reducer from './reducer'; +import saga from './sagas'; +import * as Selectors from './selectors'; +import "./styles.scss"; +import LoadingPanel from '@components/LoadingPanel'; +import Button from '@material-ui/core/Button'; +import {clearAllSubmissionsAction} from '@actions/submissions'; +import HighlightOffIcon from '@material-ui/icons/HighlightOff'; +import { withStyles } from '@material-ui/core/styles'; +import Tooltip from '@material-ui/core/Tooltip'; +import FilterDropDown from '@common/components/FilterDropDown' + +const styles = { + filterButton: { + minWidth: 150, + backgroundColor: "#0c72a0 !important ", + color: "white", + marginRight: 10, + height: 40 + }, +}; + +class SubmissionFiltersContainer extends React.PureComponent { + + componentDidMount(){ + this.props.initializeAction() + } + + onFilter = () => { + const options = this.props.submissionFilters.selectedFilters + let filterQuery = []; + Object.keys(options).forEach(key => options[key] && + filterQuery.push({"key": key, "value": options[key]}) + ) + this.props.updateFilterQuery(filterQuery) + this.props.onFilter() + } + + onFilterDelete = () => { + this.props.deleteSelectedFilters() + this.onFilter() + this.props.updateFilterQuery([]) + } + + getValue = filterKey => { + if(this.props.submissionFilters.selectedFilters && + this.props.submissionFilters.selectedFilters.hasOwnProperty(filterKey)) { + return this.props.submissionFilters.selectedFilters[filterKey].asMutable() + } + return [] + } + + renderValues = (selected, filter) => { + return filter.options + .filter(option => selected.indexOf(option.key) > -1) + .map(option => option.label) + .join(", ") + } + + handleChange = event => this.props.updateSelectedFilter(event.target.name, event.target.value); + + render() { + const { classes } = this.props; + return !this.props.submissionFilters.loading ? <div className={"filter-container"}> + {this.props.submissionFilters.filters + .filter(filter => this.props.doNotRender.indexOf(filter.filterKey) === -1 ) + .map(filter => + { + return <FilterDropDown + key={filter.label} + filter={filter} + value={this.getValue(filter.filterKey)} + handleChange={this.handleChange} + renderValues={this.renderValues} + /> + })} + + <Button + variant="contained" + size={"small"} + classes={{ root : classes.filterButton }} + onClick={this.onFilter} + > + Filter + </Button> + + <Tooltip + title={<span + style={{ fontSize : '15px'}}> + clear + </span>} + placement="right"> + <HighlightOffIcon + style={{ + visibility: Object.keys(this.props.submissionFilters.selectedFilters).length !=0 ? + 'visible' : 'hidden'}} + className={"delete-button"} + fontSize="large" + onClick={this.onFilterDelete} + /> + </Tooltip> + + </div> : <LoadingPanel /> + } +} + +SubmissionFiltersContainer.propTypes = { + submissionFilters: PropTypes.object, + initializeAction: PropTypes.func, + updateSelectedFilter: PropTypes.func, + updateFilterQuery: PropTypes.func, + onFilter: PropTypes.func, + doNotRender: PropTypes.array, + deleteSelectedFilters: PropTypes.func, + classes: PropTypes.object +} + + +const mapStateToProps = state => ({ + submissionFilters: Selectors.SelectSubmissionFiltersInfo(state), +}); + + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + { + initializeAction: Actions.initializeAction, + updateSelectedFilter: Actions.updateSelectedFilterAction, + clearAllSubmissions : clearAllSubmissionsAction, + updateFilterQuery: Actions.updateFiltersQueryAction, + deleteSelectedFilters: Actions.deleteSelectedFiltersAction + }, + dispatch, + ); +} + +const withConnect = connect( + mapStateToProps, + mapDispatchToProps, +); + +const withReducer = injectReducer({ key: 'SubmissionFiltersContainer', reducer }); +const withSaga = injectSaga({ key: 'SubmissionFiltersContainer', saga }); + + + +export default compose( + withSaga, + withReducer, + withConnect, + withRouter, + withStyles(styles) +)(SubmissionFiltersContainer); diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/models.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/models.js new file mode 100644 index 000000000..4d18088cf --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/models.js @@ -0,0 +1,10 @@ +import * as Immutable from 'seamless-immutable'; + +const initialState = Immutable.from({ + loading : true, + selectedFilters : {}, + filters: null, + filterQuery : null +}); + +export default initialState; diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/reducer.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/reducer.js new file mode 100644 index 000000000..686102414 --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/reducer.js @@ -0,0 +1,28 @@ +import * as ActionTypes from './constants'; +import initialState from './models'; + +const SubmissionFiltersReducer = (state = initialState, action) => { + switch (action.type) { + case ActionTypes.GET_FILTERS_SUCCESS: + return state.set("filters", action.data); + case ActionTypes.SHOW_LOADING: + return state.set("loading", true); + case ActionTypes.HIDE_LOADING: + return state.set("loading", false); + case ActionTypes.UPDATE_SELECTED_FILTER: + if(!(action.value).length){ + let selectedFilters = {...state.selectedFilters} + delete selectedFilters[action.filterKey] + return state.set("selectedFilters", selectedFilters); + } + return state.setIn(["selectedFilters", action.filterKey], action.value); + case ActionTypes.UPDATE_FILTERS_QUERY: + return state.set("filterQuery", action.data) + case ActionTypes.DELETE_SELECTED_FILTER: + return state.set("selectedFilters", {}) + default: + return state; + } +}; + +export default SubmissionFiltersReducer; diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/sagas.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/sagas.js new file mode 100644 index 000000000..9444b3dda --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/sagas.js @@ -0,0 +1,26 @@ +import { + call, + put, + takeEvery, +} from 'redux-saga/effects'; +import * as ActionTypes from './constants'; +import * as Actions from './actions'; +import { apiFetch } from '@api/utils' + +function* initialFetch() { + + try { + yield put(Actions.showLoadingAction()) + const response = yield call(apiFetch, {path : `/v1/submissions_filter/`}); + const data = yield response.json() + yield put(Actions.getFiltersSuccessAction(data)); + yield put(Actions.hideLoadingAction()) + } catch (e) { + console.log("error", e) + yield put(Actions.hideLoadingAction()) + } +} + +export default function* homePageSaga() { + yield takeEvery(ActionTypes.INITIALIZE, initialFetch); +} diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/selectors.js b/hypha/static_src/src/app/src/containers/SubmissionFilters/selectors.js new file mode 100644 index 000000000..b8c0bd5bd --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/selectors.js @@ -0,0 +1,9 @@ +import { createSelector } from 'reselect'; +import initialState from './models'; + +export const selectFieldsRenderer = state => + state.SubmissionFiltersContainer ? state.SubmissionFiltersContainer : initialState; + +export const SelectSubmissionFiltersInfo = createSelector(selectFieldsRenderer, domain => domain); + +export const SelectSelectedFilters = createSelector(selectFieldsRenderer, domain => domain.filterQuery) diff --git a/hypha/static_src/src/app/src/containers/SubmissionFilters/styles.scss b/hypha/static_src/src/app/src/containers/SubmissionFilters/styles.scss new file mode 100644 index 000000000..22229e4fa --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/styles.scss @@ -0,0 +1,14 @@ +.filter-container { + margin: 0 auto ; + padding: 15px; + display: flex; + justify-content: space-evenly; + align-content: center; + align-items: center; +} + +.delete-button { + cursor: pointer; +} + + diff --git a/hypha/static_src/src/app/src/redux/actions/submissions.js b/hypha/static_src/src/app/src/redux/actions/submissions.js index cfd39dd91..cf9a44c7b 100644 --- a/hypha/static_src/src/app/src/redux/actions/submissions.js +++ b/hypha/static_src/src/app/src/redux/actions/submissions.js @@ -10,7 +10,6 @@ import { import { getCurrentStatuses, - getSubmissionIDsForCurrentStatuses, } from '@selectors/statuses'; import { @@ -20,17 +19,13 @@ import { getCurrentRoundSubmissionIDs, } from '@selectors/rounds'; -import { - MESSAGE_TYPES, - addMessage, -} from '@actions/messages'; +import { SelectSelectedFilters } from '@containers/SubmissionFilters/selectors'; // Round export const UPDATE_ROUND = 'UPDATE_ROUND'; export const START_LOADING_ROUND = 'START_LOADING_ROUND'; export const FAIL_LOADING_ROUND = 'FAIL_LOADING_ROUND'; - // Rounds export const UPDATE_ROUNDS = 'UPDATE_ROUNDS'; export const START_LOADING_ROUNDS = 'START_LOADING_ROUNDS'; @@ -41,19 +36,23 @@ export const SET_CURRENT_SUBMISSION_ROUND = 'SET_CURRENT_SUBMISSION_ROUND'; 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'; +export const CLEAR_ALL_ROUNDS = 'CLEAR_ALL_ROUNDS' // 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'; +export const CLEAR_ALL_STATUSES = 'CLEAR_ALL_STATUSES' // Submissions export const SET_CURRENT_SUBMISSION = 'SET_CURRENT_SUBMISSION'; export const START_LOADING_SUBMISSION = 'START_LOADING_SUBMISSION'; export const FAIL_LOADING_SUBMISSION = 'FAIL_LOADING_SUBMISSION'; export const UPDATE_SUBMISSION = 'UPDATE_SUBMISSION'; +export const UPDATE_SUBMISSION_BY_FILTER = 'UPDATE_SUBMISSION_BY_FILTER'; export const CLEAR_CURRENT_SUBMISSION = 'CLEAR_CURRENT_SUBMISSION'; +export const CLEAR_ALL_SUBMISSIONS = 'CLEAR_ALL_SUBMISSIONS'; // Execute submission action export const START_EXECUTING_SUBMISSION_ACTION = 'START_EXECUTING_SUBMISSION_ACTION'; @@ -89,6 +88,18 @@ export const clearDeterminationDraftAction = () => ({ type: CLEAR_DETERMINATION_DRAFT, }); +export const clearAllSubmissionsAction = () => ({ + type: CLEAR_ALL_SUBMISSIONS, +}); + +export const clearAllStatusesAction = () => ({ + type: CLEAR_ALL_STATUSES, +}); + +export const clearAllRoundsAction = () => ({ + type: CLEAR_ALL_ROUNDS, +}); + export const toggleDeterminationFormAction = (status) =>({ type : TOGGLE_DETERMINATION_FORM, status @@ -135,11 +146,16 @@ export const setCurrentSubmissionRound = (id) => (dispatch) => { id, }); + dispatch(clearAllStatusesAction()) + dispatch(clearAllSubmissionsAction()) + dispatch(clearAllRoundsAction()) + return dispatch(loadCurrentRoundSubmissions()); }; export const loadSubmissionFromURL = (params) => (dispatch, getState) => { + const urlParams = new URLSearchParams(params); if (urlParams.has('submission')) { const activeId = Number(urlParams.get('submission')); @@ -197,7 +213,6 @@ export const setCurrentSubmission = id => (dispatch, getState) => { dispatch(clearCurrentDeterminationAction()) dispatch(clearDeterminationDraftAction()) dispatch(setSubmissionParam(id)); - return dispatch({ type: SET_CURRENT_SUBMISSION, id, @@ -220,7 +235,7 @@ export const loadCurrentRound = (requiredFields=[]) => (dispatch, getState) => { export const loadRounds = () => (dispatch, getState) => { const state = getState() const rounds = getRounds(state) - + if ( rounds && Object.keys(rounds).length !== 0 ) { return null } @@ -231,20 +246,12 @@ export const loadRounds = () => (dispatch, getState) => { export const loadCurrentRoundSubmissions = () => (dispatch, getState) => { const state = getState() const submissions = getCurrentRoundSubmissionIDs(state) - - if ( submissions && submissions.length !== 0 ) { + const filters = SelectSelectedFilters(state) + if ( submissions && submissions.length !== 0 && !filters ) { return null } - 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)) - } - }) + return dispatch(fetchSubmissionsByRound(getCurrentRoundID(state), filters)) } @@ -264,16 +271,18 @@ const fetchRounds = () => ({ }, }) -const fetchSubmissionsByRound = (roundID) => ({ +const fetchSubmissionsByRound = (roundID, filters) => ({ [CALL_API]: { types: [ START_LOADING_SUBMISSIONS_BY_ROUND, UPDATE_SUBMISSIONS_BY_ROUND, FAIL_LOADING_SUBMISSIONS_BY_ROUND], - endpoint: api.fetchSubmissionsByRound(roundID), + endpoint: api.fetchSubmissionsByRound(roundID, filters), }, roundID, + filters }) export const setCurrentStatuses = (statuses) => (dispatch) => { + if(!Array.isArray(statuses)) { throw new Error("Statuses have to be an array of statuses"); } @@ -282,38 +291,34 @@ export const setCurrentStatuses = (statuses) => (dispatch) => { type: SET_CURRENT_STATUSES, statuses, }); - + dispatch(clearAllRoundsAction()) + dispatch(clearAllStatusesAction()) + dispatch(clearAllSubmissionsAction()) return dispatch(loadSubmissionsForCurrentStatus()); }; -const fetchSubmissionsByStatuses = (statuses) => { +const fetchSubmissionsByStatuses = (statuses, filters) => { + return { [CALL_API]: { types: [ START_LOADING_BY_STATUSES, UPDATE_BY_STATUSES, FAIL_LOADING_BY_STATUSES], - endpoint: api.fetchSubmissionsByStatuses(statuses), + endpoint: api.fetchSubmissionsByStatuses(statuses, filters), }, statuses, + filters, }; }; export const loadSubmissionsForCurrentStatus = () => (dispatch, getState) => { const state = getState() const submissions = getCurrentStatusesSubmissions(state) - - if ( submissions && submissions.length !== 0 ) { + const filters = SelectSelectedFilters(state) + if ( submissions && submissions.length !== 0 && !filters ) { return null } - 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)) - } - }) + return dispatch(fetchSubmissionsByStatuses(getCurrentStatuses(state), filters)) } const fetchSubmission = (submissionID) => ({ diff --git a/hypha/static_src/src/app/src/redux/reducers/rounds.js b/hypha/static_src/src/app/src/redux/reducers/rounds.js index 5ce622d9b..f241b036a 100644 --- a/hypha/static_src/src/app/src/redux/reducers/rounds.js +++ b/hypha/static_src/src/app/src/redux/reducers/rounds.js @@ -9,6 +9,7 @@ import { START_LOADING_ROUND, UPDATE_ROUND, UPDATE_ROUNDS, + CLEAR_ALL_ROUNDS, FAIL_LOADING_ROUNDS, START_LOADING_ROUNDS, } from '@actions/submissions'; @@ -85,6 +86,7 @@ function roundsByID(state = {}, action) { [action.roundID]: round(state[action.roundID], action) }; case UPDATE_ROUNDS: + return { ...state, ...action.data.results.reduce((acc, value) => { @@ -95,6 +97,8 @@ function roundsByID(state = {}, action) { return acc; }, {}), }; + case CLEAR_ALL_ROUNDS: + return {} default: return state; } diff --git a/hypha/static_src/src/app/src/redux/reducers/statuses.js b/hypha/static_src/src/app/src/redux/reducers/statuses.js index 2ca81052f..d941c024d 100644 --- a/hypha/static_src/src/app/src/redux/reducers/statuses.js +++ b/hypha/static_src/src/app/src/redux/reducers/statuses.js @@ -6,6 +6,7 @@ import { START_LOADING_BY_STATUSES, FAIL_LOADING_BY_STATUSES, UPDATE_SUBMISSION, + CLEAR_ALL_STATUSES } from '@actions/submissions'; @@ -41,6 +42,8 @@ function submissionsByStatuses(state = {}, action) { ...state, [action.data.status]: [...(state[action.data.status] || []), action.data.id], }; + case CLEAR_ALL_STATUSES: + return {} default: return state } diff --git a/hypha/static_src/src/app/src/redux/reducers/submissions.js b/hypha/static_src/src/app/src/redux/reducers/submissions.js index e60332e0d..ae245ec25 100644 --- a/hypha/static_src/src/app/src/redux/reducers/submissions.js +++ b/hypha/static_src/src/app/src/redux/reducers/submissions.js @@ -20,6 +20,7 @@ import { CLEAR_CURRENT_DETERMINATION, FETCH_DETERMINATION_DRAFT, CLEAR_DETERMINATION_DRAFT, + CLEAR_ALL_SUBMISSIONS } from '@actions/submissions'; import { CREATE_NOTE, UPDATE_NOTES, UPDATE_NOTE } from '@actions/notes' @@ -110,7 +111,6 @@ function submissionsByID(state = {}, action) { }; case UPDATE_BY_STATUSES: case UPDATE_SUBMISSIONS_BY_ROUND: - // debugger return { ...state, ...action.data.results.reduce((newItems, newSubmission) => { @@ -124,6 +124,8 @@ function submissionsByID(state = {}, action) { return newItems; }, {}), }; + case CLEAR_ALL_SUBMISSIONS: + return {} default: return state; } @@ -136,6 +138,8 @@ function currentSubmission(state = null, action) { return action.id; case CLEAR_CURRENT_SUBMISSION: return null; + case CLEAR_ALL_SUBMISSIONS: + return null; default: return state; } diff --git a/hypha/static_src/src/app/src/redux/selectors/statuses.js b/hypha/static_src/src/app/src/redux/selectors/statuses.js index 70344be7e..56abad724 100644 --- a/hypha/static_src/src/app/src/redux/selectors/statuses.js +++ b/hypha/static_src/src/app/src/redux/selectors/statuses.js @@ -11,6 +11,7 @@ const getSubmissionsByStatuses = state => state.statuses.byStatuses; const getSubmissionIDsForCurrentStatuses = createSelector( [ getSubmissionsByStatuses, getCurrentStatuses ], (grouped, current) => { + if (!current.length){ let acc = [] for (let status in grouped){ diff --git a/hypha/static_src/src/app/src/redux/selectors/submissions.js b/hypha/static_src/src/app/src/redux/selectors/submissions.js index 360c9adb3..5ed3bb70d 100644 --- a/hypha/static_src/src/app/src/redux/selectors/submissions.js +++ b/hypha/static_src/src/app/src/redux/selectors/submissions.js @@ -8,6 +8,8 @@ import { getSubmissionIDsForCurrentStatuses, } from '@selectors/statuses'; +import { SelectSelectedFilters } from '@containers/SubmissionFilters/selectors'; + const getSubmissions = state => state.submissions.byID; const getCurrentSubmissionID = state => state.submissions.current; @@ -26,6 +28,7 @@ const getDeterminationDraftStatus = state => state.submissions.isDeterminationDr const getSubmissionsForListing = state => Object.values(state.submissions.byID) +const getSubmissionFilters = state => SelectSelectedFilters(state) const getCurrentRoundSubmissions = createSelector( [ getCurrentRoundSubmissionIDs, getSubmissions], @@ -38,6 +41,9 @@ const getCurrentRoundSubmissions = createSelector( const getCurrentStatusesSubmissions = createSelector( [ getSubmissionIDsForCurrentStatuses, getSubmissions], (submissionIDs, submissions) => { + if(!Object.keys(submissions).length) { + return [] + } return submissionIDs.map(submissionID => submissions[submissionID]); } ); @@ -80,4 +86,5 @@ export { getSubmissionErrorState, getSubmissionOfID, getCurrentStatusesSubmissions, + getSubmissionFilters }; -- GitLab