diff --git a/hypha/apply/api/v1/filters.py b/hypha/apply/api/v1/filters.py index 1c915c6ac8d2da3257c0fcdf94c15c5df5023ec3..4613bc17dd14c4ce0e1dc560f244327b582527fc 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 8a38f23afc1f68c5458dcc0d5559f21eccd03ac5..6d990a5626facf1ddccaa975acdf3cc6badaaebd 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 a89b6687a9501ad9085be567f092ae5e7a18b308..0682fc87b0dbd3b956f2f92c466b312650ee5f3d 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/apply/funds/templates/funds/submissions.html b/hypha/apply/funds/templates/funds/submissions.html index 0ed85d1e4044ef43e383b0c5f021a3c6219562c7..5344620603f5266e5ce47666b90ff79a9129c27f 100644 --- a/hypha/apply/funds/templates/funds/submissions.html +++ b/hypha/apply/funds/templates/funds/submissions.html @@ -1,6 +1,8 @@ {% extends "funds/base_submissions_table.html" %} -{% block title %}Submissions{% endblock %} +{% load static %} +{% load render_bundle from webpack_loader %} +{% block title %}Submissions{% endblock %} {% block content %} <div class="admin-bar"> <div class="admin-bar__inner wrapper--search"> @@ -8,13 +10,17 @@ <div> <h1 class="gamma heading heading--no-margin heading--bold">All Submissions</h1> </div> + <div id="submissions-all-react-app-switcher"></div> {% endblock %} </div> </div> -<div class="wrapper wrapper--large wrapper--inner-space-medium"> - {% block table %} - {{ block.super }} - {% endblock %} +<div id="submissions-all-react-app"> + <div class="wrapper wrapper--large wrapper--inner-space-medium"> + {% block table %} + {{ block.super }} + {% endblock %} + </div> </div> +{% render_bundle 'allSubmissions' %} {% endblock %} diff --git a/hypha/static_src/src/app/src/AllSubmissionsApp.js b/hypha/static_src/src/app/src/AllSubmissionsApp.js new file mode 100644 index 0000000000000000000000000000000000000000..f2c53cebd017c1d5c1d83c756da78d79a869aad5 --- /dev/null +++ b/hypha/static_src/src/app/src/AllSubmissionsApp.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux' +import SwitcherApp from './SwitcherApp'; +import GroupByRoundDetailView from '@containers/GroupByRoundDetailView'; +import { setCurrentStatuses } from '@actions/submissions'; +import { + getSubmissionsForListing, +} from '@selectors/submissions'; + +class AllSubmissionsApp extends React.Component { + static propTypes = { + pageContent: PropTypes.node.isRequired, + setStatuses: PropTypes.func.isRequired, + submissions: PropTypes.array, + doNotRenderFilter: PropTypes.array + }; + + + componentDidMount() { + this.props.setStatuses([]); + } + + onfilter = () => { + this.props.setStatuses([]) + } + + render() { + return ( + <SwitcherApp + detailComponent={<GroupByRoundDetailView submissions= {this.props.submissions} groupBy="all"/>} + switcherSelector={'submissions-all-react-app-switcher'} + doNotRenderFilter={[]} + pageContent={this.props.pageContent} + onFilter={this.onfilter} /> + ) + } +} + +const mapStateToProps = (state, ownProps) => ({ + submissions: getSubmissionsForListing(state), +}) + +const mapDispatchToProps = dispatch => { + return { + setStatuses: statuses => {dispatch(setCurrentStatuses(statuses));}, + } +}; + +export default hot(module)( + connect(mapStateToProps, mapDispatchToProps)(AllSubmissionsApp) +); diff --git a/hypha/static_src/src/app/src/SubmissionsByRoundApp.js b/hypha/static_src/src/app/src/SubmissionsByRoundApp.js index 26cdc348fb7729ea6c49994f66fe16f838f5e667..fff434a258412ec6993e66cb2923ce593ccdd65e 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 90c6b8989f4665e129841af6da44a87134142410..e2f757cd012b23777430014e6c3d328f02b9c59c 100644 --- a/hypha/static_src/src/app/src/SubmissionsByStatusApp.js +++ b/hypha/static_src/src/app/src/SubmissionsByStatusApp.js @@ -6,6 +6,7 @@ import { connect } from 'react-redux' import GroupByRoundDetailView from '@containers/GroupByRoundDetailView'; import { setCurrentStatuses } from '@actions/submissions'; +import { getCurrentStatusesSubmissions } from '@selectors/submissions'; class SubmissionsByStatusApp extends React.Component { @@ -13,27 +14,40 @@ class SubmissionsByStatusApp extends React.Component { pageContent: PropTypes.node.isRequired, statuses: PropTypes.arrayOf(PropTypes.string), setStatuses: PropTypes.func.isRequired, + submissions: PropTypes.array }; componentDidMount() { this.props.setStatuses(this.props.statuses); } + onfilter = () => { + this.props.setStatuses(this.props.statuses); + } + render() { return <SwitcherApp - detailComponent={<GroupByRoundDetailView />} + 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} />; } } +const mapStateToProps = (state, ownProps) => ({ + submissions: getCurrentStatusesSubmissions(state), +}) + const mapDispatchToProps = dispatch => { return { - setStatuses: statuses => {dispatch(setCurrentStatuses(statuses));}, + setStatuses: (statuses) => { + dispatch(setCurrentStatuses(statuses)); + }, } }; export default hot(module)( - connect(null, mapDispatchToProps)(SubmissionsByStatusApp) + connect(mapStateToProps, mapDispatchToProps)(SubmissionsByStatusApp) ); diff --git a/hypha/static_src/src/app/src/SwitcherApp.js b/hypha/static_src/src/app/src/SwitcherApp.js index 20f58497d9ac6ddf8e7cc06f3228f535f4fa7724..dbaa059d50ff899d5187ca03557ecc776a526636 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 = { @@ -47,9 +50,8 @@ class SwitcherApp extends React.Component { componentDidUpdate(prevProps) { - if (prevProps.searchParam !== this.props.searchParam) { + if (prevProps.searchParam !== this.props.searchParam && !document.body.classList.contains('app-open')){ const success = this.props.processParams(this.props.searchParam) - if (!success) { this.closeDetail() } else { @@ -91,7 +93,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/allSubmissionsIndex.js b/hypha/static_src/src/app/src/allSubmissionsIndex.js new file mode 100644 index 0000000000000000000000000000000000000000..728a687510d82ce5d80ee345f7c7e70ab5fa8b77 --- /dev/null +++ b/hypha/static_src/src/app/src/allSubmissionsIndex.js @@ -0,0 +1,24 @@ +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 AllSubmissionsApp from './AllSubmissionsApp'; +import createStore, { history } from '@redux/store'; + + +const container = document.getElementById('submissions-all-react-app'); + +const store = createStore(); + +Modal.setAppElement(container) + +ReactDOM.render( + <Provider store={store}> + <ConnectedRouter history={history}> + <AllSubmissionsApp pageContent={container.innerHTML}/> + </ConnectedRouter> + </Provider>, + container +); diff --git a/hypha/static_src/src/app/src/api/index.js b/hypha/static_src/src/app/src/api/index.js index 320423098b29ecb12c49a30805931b95d280bdb9..f9939fea776f3b905a32c2f69eab75500813556c 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 fc51f4d013f44c3122d097c90fe9214b902b031a..257fa6d7ca7785912859b20e09289bd206669e0b 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 0000000000000000000000000000000000000000..91a868941ef6a1bf715e98889370683952fd0b81 --- /dev/null +++ b/hypha/static_src/src/app/src/common/components/FilterDropDown/index.js @@ -0,0 +1,66 @@ +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: { + width: '100%', + 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 43c72b22d8682ff2491d1e3f293949e65e9f6a11..1674b030c03cd04d595fdad4cf2cf8dff2ce53b4 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,10 +108,9 @@ export default class GroupedListing extends React.Component { </ListingGroup> ); } - + render() { const { isLoading, isErrored, errorMessage } = this.props; - const passProps = { items: this.state.orderedItems, renderItem: this.renderItem, @@ -121,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 db881d048ddc84149bf98a527dde386dd036aba3..428f0c35deff8b113cf07c5654eb7649c55e4937 100644 --- a/hypha/static_src/src/app/src/components/GroupedListing/styles.scss +++ b/hypha/static_src/src/app/src/components/GroupedListing/styles.scss @@ -2,7 +2,6 @@ height: 100vh; display: flex; flex-direction: column; - @include media-query(tablet-landscape) { height: calc(100vh - var(--header-admin-height)); } @@ -15,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 e9d1778f64b742a3b74f09c4ecde5e96577d234b..969c453c4f87b0f85ed5cf3dadcd96247305d723 100644 --- a/hypha/static_src/src/app/src/containers/ByRoundListing.js +++ b/hypha/static_src/src/app/src/containers/ByRoundListing.js @@ -16,6 +16,7 @@ import { import { getCurrentSubmissionID, getCurrentStatusesSubmissions, + getSubmissionsForListing, } from '@selectors/submissions'; import { getCurrentStatuses, @@ -23,6 +24,7 @@ import { getByStatusesError, } from '@selectors/statuses'; +import { SelectSelectedFilters } from '@containers/SubmissionFilters/selectors' const loadData = props => { props.loadRounds() @@ -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) } } @@ -89,9 +92,9 @@ class ByRoundListing extends React.Component { } } -const mapStateToProps = (state) => ({ +const mapStateToProps = (state, ownProps) => ({ statuses: getCurrentStatuses(state), - submissions: getCurrentStatusesSubmissions(state), + submissions: ownProps.groupBy ? getSubmissionsForListing(state) : getCurrentStatusesSubmissions(state), isErrored: getRoundsErrored(state) || getByStatusesError(state), isLoading: ( getByStatusesLoading(state) || @@ -99,6 +102,7 @@ const mapStateToProps = (state) => ({ ), 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 573090c9877cf299dcf9da650f294f8ecda6ac69..55f2f675da259b837f02cbf3b6232df2ad887bf7 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/GroupByRoundDetailView.js b/hypha/static_src/src/app/src/containers/GroupByRoundDetailView.js index a5492c4afdc14f771b14db0b0a500ea8ab6151ea..a49804989d96d953b5b75cb0df0f553d4a98ea8c 100644 --- a/hypha/static_src/src/app/src/containers/GroupByRoundDetailView.js +++ b/hypha/static_src/src/app/src/containers/GroupByRoundDetailView.js @@ -9,7 +9,6 @@ import { getRoundsErrored, } from '@selectors/rounds' import { - getCurrentStatusesSubmissions, getCurrentSubmissionID, } from '@selectors/submissions'; import { @@ -18,17 +17,16 @@ import { } from '@selectors/statuses'; const GroupByRoundDetailView = props => { - const listing = <ByRoundListing submissionStatuses={props.submissionStatuses} /> + const listing = <ByRoundListing submissionStatuses={props.submissionStatuses} groupBy = {props.groupBy && props.groupBy}/> const { isLoading, isErrored, submissions, submissionID, errorMessage } = props const isEmpty = submissions.length === 0 - const activeSubmision = !!submissionID - + const activeSubmission = !!submissionID return ( <DetailView isEmpty={isEmpty} listing={listing} isLoading={isLoading} - showSubmision={activeSubmision} + showSubmision={activeSubmission} isErrored={isErrored} errorMessage={errorMessage} /> @@ -42,6 +40,7 @@ GroupByRoundDetailView.propTypes = { isLoading: PropTypes.bool, isErrored: PropTypes.bool, errorMessage: PropTypes.string, + groupBy: PropTypes.string } const mapStateToProps = (state, ownProps) => ({ @@ -49,7 +48,6 @@ const mapStateToProps = (state, ownProps) => ({ isLoading: ( getByStatusesLoading(state) || getRoundsFetching(state) ), - submissions: getCurrentStatusesSubmissions(state), submissionID: getCurrentSubmissionID(state), }) diff --git a/hypha/static_src/src/app/src/containers/ReviewInformation.js b/hypha/static_src/src/app/src/containers/ReviewInformation.js index 0ccd9e89949b2649e9423dfd79d9e7bad07b33fc..12ffb818501b010a5df28ac0cdf00611ad0f66f2 100644 --- a/hypha/static_src/src/app/src/containers/ReviewInformation.js +++ b/hypha/static_src/src/app/src/containers/ReviewInformation.js @@ -162,7 +162,7 @@ const ReviewInformation = ({ submission, submissionID, showReviewForm, toggleRev ReviewInformation.propTypes = { submission: PropTypes.object, - submissionID: PropTypes.number.isRequired, + submissionID: PropTypes.number, showReviewForm: PropTypes.bool, reviewDraftStatus: PropTypes.bool, toggleReviewForm: PropTypes.func, diff --git a/hypha/static_src/src/app/src/containers/ScreeningStatus/index.js b/hypha/static_src/src/app/src/containers/ScreeningStatus/index.js index 39918ba31180515627273cf5db3be06c2f2e9cce..a446e73f7faef282fe4dd2c1bd162f2ff9d13876 100644 --- a/hypha/static_src/src/app/src/containers/ScreeningStatus/index.js +++ b/hypha/static_src/src/app/src/containers/ScreeningStatus/index.js @@ -88,7 +88,7 @@ class ScreeningStatusContainer extends React.PureComponent { label={option.title} variant={!option.selected ? "outlined" : "default"} key={option.id} - icon={option.selected && <DoneIcon />} + icon={option.selected ? <DoneIcon /> : null} onClick={() => selectVisibleOption(submissionID, option)}> </Chip>) } 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 f8b570a67bae40817f789a37c806db63a4ec07d2..6a5c9234ce3cb50449fc6c2f89d5548526896ee7 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/StatusActions.js b/hypha/static_src/src/app/src/containers/StatusActions.js index 1f8de8db3e817313206abf4deb4be0552b509320..7f926bfa39cc74b13d26e2dd418e8a6e5aae0b01 100644 --- a/hypha/static_src/src/app/src/containers/StatusActions.js +++ b/hypha/static_src/src/app/src/containers/StatusActions.js @@ -15,7 +15,7 @@ import './StatusActions.scss'; class StatusActions extends React.Component { static propTypes = { - submissionID: PropTypes.number.isRequired, + submissionID: PropTypes.number, submission: PropTypes.shape({ id: PropTypes.number, phase: PropTypes.string, 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 0000000000000000000000000000000000000000..f9e1dacf066cfdf89bff3048092f3559346d6772 --- /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 0000000000000000000000000000000000000000..6528486c0599c0095e5a2adf51f76d416250ed0f --- /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 0000000000000000000000000000000000000000..fb966c452676702887c35dcc7c1b44dd9bfe703f --- /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 0000000000000000000000000000000000000000..4d18088cfbe209214641473607c31272baeef7ee --- /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 0000000000000000000000000000000000000000..686102414da1134b8acc3bdc4ac67392f43c4015 --- /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 0000000000000000000000000000000000000000..9444b3dda101aafd2d97d12e3a4fb863a95bc6a5 --- /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 0000000000000000000000000000000000000000..b8c0bd5bd7c1e6b0388d0e705beda73c849c4c86 --- /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 0000000000000000000000000000000000000000..e093b58445d5272eb9b27364f0cf4cabcb946f7c --- /dev/null +++ b/hypha/static_src/src/app/src/containers/SubmissionFilters/styles.scss @@ -0,0 +1,11 @@ +.filter-container { + padding: 15px; + display: flex; + justify-content: space-between; +} + +.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 919c442f348b11297b23536001e5a351deaa38bb..1951058f94a743e2cc3f40b6a5a113c4833cff4f 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 @@ -105,7 +116,7 @@ export const clearCurrentDeterminationAction = () => ({ export const fetchReviewDraft = (submissionID) => ({ [CALL_API]: { - types: [ START_LOADING_SUBMISSION, FETCH_REVIEW_DRAFT, FAIL_LOADING_SUBMISSION], + types: [ START_LOADING_SUBMISSION, FETCH_REVIEW_DRAFT, CLEAR_REVIEW_DRAFT], endpoint: api.fetchReviewDraft(submissionID), }, submissionID, @@ -135,16 +146,20 @@ 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')); const submissionID = getCurrentSubmissionID(getState()); - if (activeId !== null && submissionID !== activeId) { dispatch(setCurrentSubmission(activeId)); } @@ -172,7 +187,6 @@ const setSubmissionParam = (id) => (dispatch, getState) => { const shouldSet = !urlID && !!id; const shouldUpdate = id !== null && submissionID !== id && urlID !== id; - if (shouldSet || shouldUpdate) { dispatch(push({search: `?submission=${id}`})); } else if (id === null) { @@ -197,7 +211,6 @@ export const setCurrentSubmission = id => (dispatch, getState) => { dispatch(clearCurrentDeterminationAction()) dispatch(clearDeterminationDraftAction()) dispatch(setSubmissionParam(id)); - return dispatch({ type: SET_CURRENT_SUBMISSION, id, @@ -220,7 +233,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 +244,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 +269,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 +289,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 5ce622d9b500f2349091fbbcff875fbc340451c4..f241b036a4c2ad3b01a094e4d22b3724d0344050 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 2ca81052f274ba34b316f205309feb1bcc52732b..d941c024df89f9c530a60b2e48b0c03fa1decdab 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 e60332e0d5f28c1361f2ffbdb962a5c249157a78..ae245ec25b0b2760233b6a7bf45fd56236962388 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 ff17929c7c7242b7c34cd566814fb1d6416ae6f1..56abad7241e4a67972ef277bb42d823f16386dfe 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,14 @@ const getSubmissionsByStatuses = state => state.statuses.byStatuses; const getSubmissionIDsForCurrentStatuses = createSelector( [ getSubmissionsByStatuses, getCurrentStatuses ], (grouped, current) => { + + if (!current.length){ + let acc = [] + for (let status in grouped){ + acc = acc.concat(grouped[status]) + } + return acc + } return current.reduce((acc, status) => acc.concat(grouped[status] || []), []) } ); 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 5bcb0d6f9a0cc79db6f5b09bc814ab36f750d335..5ed3bb70d1a3350f8c713e8f5bb1d725a5200533 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; @@ -24,6 +26,9 @@ const getCurrentDetermination = state => state.submissions.currentDetermination; const getDeterminationDraftStatus = state => state.submissions.isDeterminationDraftExist; +const getSubmissionsForListing = state => Object.values(state.submissions.byID) + +const getSubmissionFilters = state => SelectSelectedFilters(state) const getCurrentRoundSubmissions = createSelector( [ getCurrentRoundSubmissionIDs, getSubmissions], @@ -36,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]); } ); @@ -61,6 +69,8 @@ const getSubmissionsByRoundError = state => state.rounds.error; const getSubmissionsByRoundLoadingState = state => state.submissions.itemsLoading === true; export { + getSubmissions, + getSubmissionsForListing, getCurrentRoundSubmissions, getCurrentSubmission, getCurrentSubmissionID, @@ -76,4 +86,5 @@ export { getSubmissionErrorState, getSubmissionOfID, getCurrentStatusesSubmissions, + getSubmissionFilters }; diff --git a/hypha/static_src/src/app/webpack.base.config.js b/hypha/static_src/src/app/webpack.base.config.js index 96429b96707987cfbe03220eb4359dc54ae0c074..91f22855e20eff1361d5954292b76255e8d9dd41 100644 --- a/hypha/static_src/src/app/webpack.base.config.js +++ b/hypha/static_src/src/app/webpack.base.config.js @@ -15,6 +15,7 @@ module.exports = (webpackEnv) => { entry: { submissionsByRound: COMMON_ENTRY.concat(['./src/submissionsByRoundIndex']), submissionsByStatus: COMMON_ENTRY.concat(['./src/submissionsByStatusIndex']), + allSubmissions: COMMON_ENTRY.concat(['./src/allSubmissionsIndex']) }, output: {