diff --git a/src/components/forms/Search/index.tsx b/src/components/forms/Search/index.tsx new file mode 100644 index 0000000000..37882326ca --- /dev/null +++ b/src/components/forms/Search/index.tsx @@ -0,0 +1,71 @@ +import { History, Location } from 'history'; +import React, { useEffect, useState } from 'react'; +import { Button, Form, FormGroup, Input } from 'reactstrap'; +import { SEARCH } from '../../../configs/lang'; +import { QUERY_PARAM_TITLE } from '../../../constants'; +import { getQueryParams } from '../../../helpers/utils'; + +/** + * Interface for handleSearchChange event handler + */ +export type Change = (event: React.ChangeEvent) => void; + +/** + * Interface for handleSubmit event handler + */ +export type Submit = (event: React.FormEvent) => void; + +/** + * Interface for SearchForm props + */ +export interface SearchFormProps { + history: History; + location: Location; + placeholder: string; +} + +/** + * default props for SerchForm component + */ +export const defaultSearchFormProps = { + placeholder: SEARCH, +}; + +/** SearchForm component */ +export const SearchForm = (props: SearchFormProps) => { + const [searchQuery, setSearchQuery] = useState(''); + + useEffect(() => { + setSearchQuery(getQueryParams(props.location)[QUERY_PARAM_TITLE] as string); + }, []); + + const handleSearchChange: Change = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + + const handleSubmit: Submit = (event: React.FormEvent) => { + event.preventDefault(); + props.history.push({ + search: `?${QUERY_PARAM_TITLE}=${searchQuery}`, + }); + }; + + return ( +
+ + + + +
+ ); +}; + +SearchForm.defaultProps = defaultSearchFormProps; diff --git a/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap b/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..266994af4e --- /dev/null +++ b/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/components/SearchForm renders correctly 1`] = ` + +
+ + +
+ + + +
+
+ + +
+ +
+`; diff --git a/src/components/forms/Search/tests/index.test.tsx b/src/components/forms/Search/tests/index.test.tsx new file mode 100644 index 0000000000..9257f12f2d --- /dev/null +++ b/src/components/forms/Search/tests/index.test.tsx @@ -0,0 +1,46 @@ +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import { createBrowserHistory } from 'history'; +import React from 'react'; +import { SearchForm } from '../../Search'; + +const history = createBrowserHistory(); + +describe('src/components/SearchForm', () => { + it('renders correctly', () => { + const props = { + handleSearchChange: jest.fn(), + history, + location: { hash: '', pathname: '/', search: '', state: undefined, query: {} }, + }; + const wrapper = mount(); + expect(toJson(wrapper)).toMatchSnapshot(); + wrapper.unmount(); + }); + + it('handles submit correctly', () => { + jest.spyOn(history, 'push'); + const props = { + handleSearchChange: jest.fn(), + history, + location: { hash: '', pathname: '/', search: '', state: undefined, query: {} }, + }; + const wrapper = mount(); + wrapper.find('Input').simulate('change', { target: { value: 'test' } }); + wrapper.find('Form').simulate('submit'); + expect(history.push).toBeCalledWith({ search: '?title=test' }); + wrapper.unmount(); + }); + + it('displays placeholder correctly', () => { + const props = { + handleSearchChange: jest.fn(), + history, + location: { hash: '', pathname: '/', search: '', state: undefined, query: {} }, + placeholder: 'Search me', + }; + const wrapper = mount(); + expect(wrapper.find('Input').prop('placeholder')).toEqual('Search me'); + wrapper.unmount(); + }); +}); diff --git a/src/constants.tsx b/src/constants.tsx index 8e6a855fea..a351d1fe9f 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -122,3 +122,6 @@ export const PRACTITIONER_CODE = { }; /** Field to sort plans by */ export const SORT_BY_EFFECTIVE_PERIOD_START_FIELD = 'plan_effective_period_start'; + +/** Query Params */ +export const QUERY_PARAM_TITLE = 'title'; diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index e64ea4e4c9..c8a2eb0a3b 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -11,10 +11,11 @@ import { RouteComponentProps } from 'react-router'; import { Link } from 'react-router-dom'; import { CellInfo, Column } from 'react-table'; import 'react-table/react-table.css'; -import { Button, Col, Form, FormGroup, Input, Row, Table } from 'reactstrap'; +import { Col, Row, Table } from 'reactstrap'; import { Store } from 'redux'; import { format } from 'util'; import DrillDownTableLinkedCell from '../../../../components/DrillDownTableLinkedCell'; +import { SearchForm } from '../../../../components/forms/Search'; import LinkAsButton from '../../../../components/LinkAsButton'; import NewRecordBadge from '../../../../components/NewRecordBadge'; import HeaderBreadCrumb, { @@ -40,8 +41,6 @@ import { PREVIOUS, REACTIVE, ROUTINE_TITLE, - SEARCH, - SEARCH_ACTIVE_FOCUS_INVESTIGATIONS, START_DATE, STATUS_HEADER, } from '../../../../configs/lang'; @@ -56,6 +55,7 @@ import { FI_SINGLE_URL, FI_URL, HOME_URL, + QUERY_PARAM_TITLE, ROUTINE, } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; @@ -64,6 +64,7 @@ import '../../../../helpers/tables.css'; import { defaultTableProps, getFilteredFIPlansURL, + getQueryParams, removeNullJurisdictionPlans, } from '../../../../helpers/utils'; import { extractPlan, getLocationColumns } from '../../../../helpers/utils'; @@ -71,8 +72,8 @@ import supersetFetch from '../../../../services/superset'; import plansReducer, { fetchPlans, getPlanById, - getPlansArray, InterventionType, + makePlansArraySelector, Plan, PlanStatus, reducerName as plansReducerName, @@ -95,6 +96,7 @@ export interface ActiveFIProps { routinePlans: Plan[] | null; supersetService: typeof supersetFetch; plan: Plan | null; + searchedTitle: string | null; } /** default props for ActiveFI component */ @@ -103,6 +105,7 @@ export const defaultActiveFIProps: ActiveFIProps = { fetchPlansActionCreator: fetchPlans, plan: null, routinePlans: null, + searchedTitle: null, supersetService: supersetFetch, }; @@ -112,6 +115,7 @@ class ActiveFocusInvestigation extends React.Component< {} > { public static defaultProps: ActiveFIProps = defaultActiveFIProps; + constructor(props: ActiveFIProps & RouteComponentProps) { super(props); } @@ -125,9 +129,7 @@ class ActiveFocusInvestigation extends React.Component< .then((result: Plan[]) => fetchPlansActionCreator(result)) .catch(err => displayError(err)); } - public handleSubmit(event: React.FormEvent) { - event.preventDefault(); - } + public render() { const breadcrumbProps: BreadCrumbProps = { currentPage: { @@ -146,7 +148,7 @@ class ActiveFocusInvestigation extends React.Component< url: HOME_URL, }; - const { caseTriggeredPlans, routinePlans, plan } = this.props; + const { caseTriggeredPlans, routinePlans, plan, searchedTitle } = this.props; // We need to initialize jurisdictionName to a falsy value let jurisdictionName = null; @@ -176,7 +178,8 @@ class ActiveFocusInvestigation extends React.Component< caseTriggeredPlans && caseTriggeredPlans.length === 0 && routinePlans && - routinePlans.length === 0 + routinePlans.length === 0 && + searchedTitle === null ) { return ; } @@ -192,19 +195,7 @@ class ActiveFocusInvestigation extends React.Component<

{pageTitle}


-
- - - - -
+ {[caseTriggeredPlans, routinePlans].forEach((plansArray: Plan[] | null, i) => { const locationColumns: Column[] = getLocationColumns(locationHierarchy, true); if (plansArray && plansArray.length) { @@ -402,7 +393,7 @@ class ActiveFocusInvestigation extends React.Component< columns: emptyPlansColumns, }; routineReactivePlans.push( - + ); } })} @@ -425,6 +416,7 @@ interface DispatchedStateProps { plan: Plan | null; caseTriggeredPlans: Plan[] | null; routinePlans: Plan[] | null; + searchedTitle: string; } /** map state to props */ @@ -436,26 +428,28 @@ const mapStateToProps = (state: Partial, ownProps: any): DispatchedStateP ownProps.match.params && ownProps.match.params.jurisdiction_parent_id ? ownProps.match.params.jurisdiction_parent_id : null; - const caseTriggeredPlans = getPlansArray( - state, - InterventionType.FI, - [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - CASE_TRIGGERED, - [], - jurisdictionParentId - ); - const routinePlans = getPlansArray( - state, - InterventionType.FI, - [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - ROUTINE, - [], - jurisdictionParentId - ); + + const searchedTitle = getQueryParams(ownProps.location)[QUERY_PARAM_TITLE] as string; + const caseTriggeredPlans = makePlansArraySelector()(state, { + interventionType: InterventionType.FI, + parentJurisdictionId: jurisdictionParentId, + reason: CASE_TRIGGERED, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: searchedTitle, + }); + const routinePlans = makePlansArraySelector()(state, { + interventionType: InterventionType.FI, + parentJurisdictionId: jurisdictionParentId, + reason: ROUTINE, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: searchedTitle, + }); + return { caseTriggeredPlans, plan, routinePlans, + searchedTitle, }; }; diff --git a/src/containers/pages/FocusInvestigation/active/tests/__snapshots__/index.test.tsx.snap b/src/containers/pages/FocusInvestigation/active/tests/__snapshots__/index.test.tsx.snap index 1dbc611eec..cc9793c78a 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/__snapshots__/index.test.tsx.snap +++ b/src/containers/pages/FocusInvestigation/active/tests/__snapshots__/index.test.tsx.snap @@ -1,5 +1,122 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`containers/pages/ActiveFocusInvestigation handles case insensitive searches correctly 1`] = ` +Array [ + Object { + "canton": "Chadiza", + "caseClassification": null, + "caseNotificationDate": null, + "district": null, + "focusArea": "NVI_439", + "id": "ed2b4b7c-3388-53d9-b9f6-6a19d1ffde1f", + "jurisdiction_depth": 2, + "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", + "jurisdiction_name": "NVI_439", + "jurisdiction_name_path": Array [ + "Chadiza", + "Naviluli", + ], + "jurisdiction_parent_id": "2944", + "jurisdiction_path": Array [ + "2939", + "2944", + ], + "plan_date": "2019-06-18", + "plan_effective_period_end": "2019-06-18", + "plan_effective_period_start": "2019-07-31", + "plan_fi_reason": "Routine", + "plan_fi_status": "A1", + "plan_id": "10f9e9fa-ce34-4b27-a961-72fab5206ab6", + "plan_intervention_type": "FI", + "plan_status": "active", + "plan_title": "A1-Tha Luang Village 1 Focus 01", + "plan_version": "1", + "province": null, + "reason": "Routine", + "status": "A1", + "village": "Naviluli", + }, +] +`; + +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans 1`] = ` +Array [ + Object { + "canton": "Chadiza", + "caseClassification": null, + "caseNotificationDate": "2019-06-18", + "district": null, + "focusArea": "NVI_439", + "id": "plan-25", + "jurisdiction_depth": 2, + "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", + "jurisdiction_name": "NVI_439", + "jurisdiction_name_path": Array [ + "Chadiza", + "Naviluli", + ], + "jurisdiction_parent_id": "2944", + "jurisdiction_path": Array [ + "2939", + "2944", + ], + "plan_date": "2019-06-18", + "plan_effective_period_end": "2019-06-18", + "plan_effective_period_start": "2019-07-31", + "plan_fi_reason": "Case Triggered", + "plan_fi_status": "A1", + "plan_id": "10f9e9fa-ce34-4b27-a961-72fab5206ab6", + "plan_intervention_type": "FI", + "plan_status": "active", + "plan_title": "Test by Jane Doe", + "plan_version": "1", + "province": null, + "reason": "Case Triggered", + "status": "A1", + "village": "Naviluli", + }, +] +`; + +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for routine plans 1`] = ` +Array [ + Object { + "canton": "Chadiza", + "caseClassification": null, + "caseNotificationDate": null, + "district": null, + "focusArea": "NVI_439", + "id": "ed2b4b7c-3388-53d9-b9f6-6a19d1ffde1f", + "jurisdiction_depth": 2, + "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", + "jurisdiction_name": "NVI_439", + "jurisdiction_name_path": Array [ + "Chadiza", + "Naviluli", + ], + "jurisdiction_parent_id": "2944", + "jurisdiction_path": Array [ + "2939", + "2944", + ], + "plan_date": "2019-06-18", + "plan_effective_period_end": "2019-06-18", + "plan_effective_period_start": "2019-07-31", + "plan_fi_reason": "Routine", + "plan_fi_status": "A1", + "plan_id": "10f9e9fa-ce34-4b27-a961-72fab5206ab6", + "plan_intervention_type": "FI", + "plan_status": "active", + "plan_title": "A1-Tha Luang Village 1 Focus 01", + "plan_version": "1", + "province": null, + "reason": "Routine", + "status": "A1", + "village": "Naviluli", + }, +] +`; + exports[`containers/pages/ActiveFocusInvestigation works with the Redux store 1`] = ` Object { "aggregated": undefined, diff --git a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx index 4d83961189..efde41debe 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx +++ b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx @@ -10,8 +10,13 @@ import { Helmet } from 'react-helmet'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; import { CURRENT_FOCUS_INVESTIGATION } from '../../../../../configs/lang'; +import { FI_URL } from '../../../../../constants'; import store from '../../../../../store'; -import reducer, { fetchPlans, reducerName } from '../../../../../store/ducks/plans'; +import reducer, { + fetchPlans, + reducerName, + removePlansAction, +} from '../../../../../store/ducks/plans'; import { InterventionType } from '../../../../../store/ducks/plans'; import * as fixtures from '../../../../../store/ducks/tests/fixtures'; import ConnectedActiveFocusInvestigation, { ActiveFocusInvestigation } from '../../active'; @@ -25,6 +30,7 @@ jest.mock('../../../../../configs/env'); describe('containers/pages/ActiveFocusInvestigation', () => { beforeEach(() => { jest.resetAllMocks(); + store.dispatch(removePlansAction); MockDate.reset(); }); @@ -230,4 +236,140 @@ describe('containers/pages/ActiveFocusInvestigation', () => { expect(supersetMock).toHaveBeenCalledWith(0, supersetParams); wrapper.unmount(); }); + + it('handles search correctly for case triggered plans', async () => { + store.dispatch(fetchPlans([fixtures.plan24, fixtures.plan25])); + + const props = { + history, + location: { + pathname: FI_URL, + search: '?title=Jane', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('ReactTable') + .at(0) + .prop('data') + ).toMatchSnapshot(); + }); + + it('handles search correctly for routine plans', async () => { + store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22])); + + const props = { + history, + location: { + pathname: FI_URL, + search: '?title=Luang', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('ReactTable') + .at(1) + .prop('data') + ).toMatchSnapshot(); + wrapper.unmount(); + }); + + it('handles case insensitive searches correctly', async () => { + store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22])); + + const props = { + history, + location: { + pathname: FI_URL, + search: '?title=LUANG', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('ReactTable') + .at(1) + .prop('data') + ).toMatchSnapshot(); + wrapper.unmount(); + }); + + it('renders empty tables if search query does not match any case trigger or routine plans', async () => { + store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22, fixtures.plan24, fixtures.plan25])); + + const props = { + history, + location: { + pathname: FI_URL, + search: '?title=Amazon', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('ReactTable') + .at(0) + .prop('data') + ).toEqual([]); + expect( + wrapper + .find('ReactTable') + .at(1) + .prop('data') + ).toEqual([]); + }); }); diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index 286db52903..0c380f24cd 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -3,9 +3,11 @@ import reducerRegistry from '@onaio/redux-reducer-registry'; import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router'; import { Link } from 'react-router-dom'; import { Col, Row } from 'reactstrap'; import { Store } from 'redux'; +import { SearchForm } from '../../../../components/forms/Search'; import HeaderBreadcrumb from '../../../../components/page/HeaderBreadcrumb/HeaderBreadcrumb'; import Loading from '../../../../components/page/Loading'; import { SUPERSET_IRS_REPORTING_PLANS_SLICE } from '../../../../configs/env'; @@ -19,13 +21,14 @@ import { TITLE, } from '../../../../configs/lang'; import { planStatusDisplay } from '../../../../configs/settings'; -import { HOME_URL, REPORT_IRS_PLAN_URL } from '../../../../constants'; +import { HOME_URL, QUERY_PARAM_TITLE, REPORT_IRS_PLAN_URL } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; +import { getQueryParams } from '../../../../helpers/utils'; import supersetFetch from '../../../../services/superset'; import IRSPlansReducer, { fetchIRSPlans, - getIRSPlansArray, IRSPlan, + makeIRSPlansArraySelector, reducerName as IRSPlansReducerName, } from '../../../../store/ducks/generic/plans'; @@ -40,12 +43,10 @@ interface PlanListProps { } /** Simple component that loads the new plan form and allows you to create a new plan */ -const IRSPlansList = (props: PlanListProps) => { +const IRSPlansList = (props: PlanListProps & RouteComponentProps) => { const { fetchPlans, plans, service } = props; - const [loading, setLoading] = useState(true); - + const [loading, setLoading] = useState(false); const pageTitle: string = IRS_PLANS; - const breadcrumbProps = { currentPage: { label: pageTitle, @@ -77,12 +78,8 @@ const IRSPlansList = (props: PlanListProps) => { loadData().catch(error => displayError(error)); }, []); - if (loading === true) { - return ; - } - - const listViewProps = { - data: plans.map(planObj => { + const listViewData = (planList: IRSPlan[]) => + planList.map(planObj => { return [ {planObj.plan_title} @@ -92,7 +89,14 @@ const IRSPlansList = (props: PlanListProps) => { planObj.plan_effective_period_end, planStatusDisplay[planObj.plan_status] || planObj.plan_status, ]; - }), + }); + + if (loading === true) { + return ; + } + + const listViewProps = { + data: listViewData(plans), headerItems: [TITLE, DATE_CREATED, START_DATE, END_DATE, STATUS_HEADER], tableClass: 'table table-bordered plans-list', }; @@ -108,6 +112,9 @@ const IRSPlansList = (props: PlanListProps) => {

{pageTitle}

+
+ + @@ -136,11 +143,12 @@ interface DispatchedStateProps { } /** map state to props */ -const mapStateToProps = (state: Partial): DispatchedStateProps => { - const planDefinitionsArray = getIRSPlansArray(state); +const mapStateToProps = (state: Partial, ownProps: any): DispatchedStateProps => { + const searchedTitle = getQueryParams(ownProps.location)[QUERY_PARAM_TITLE] as string; + const IRSPlansArray = makeIRSPlansArraySelector()(state, { plan_title: searchedTitle }); return { - plans: planDefinitionsArray, + plans: IRSPlansArray, }; }; diff --git a/src/containers/pages/IRS/plans/tests/index.test.tsx b/src/containers/pages/IRS/plans/tests/index.test.tsx index 025b1badb0..57090e8b8a 100644 --- a/src/containers/pages/IRS/plans/tests/index.test.tsx +++ b/src/containers/pages/IRS/plans/tests/index.test.tsx @@ -2,9 +2,13 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { createBrowserHistory } from 'history'; import React from 'react'; +import { Provider } from 'react-redux'; import { Router } from 'react-router'; -import { IRSPlansList } from '../'; +import ConnectedIRSPlansList, { IRSPlansList } from '../'; +import { REPORT_IRS_PLAN_URL } from '../../../../../constants'; +import store from '../../../../../store'; import { IRSPlan } from '../../../../../store/ducks/generic/plans'; +import { fetchIRSPlans } from '../../../../../store/ducks/generic/plans'; import * as fixtures from '../../../../../store/ducks/generic/tests/fixtures'; /* tslint:disable-next-line no-var-requires */ @@ -19,6 +23,19 @@ describe('components/IRS Reports/IRSPlansList', () => { it('renders without crashing', () => { const props = { + history, + location: { + hash: '', + pathname: REPORT_IRS_PLAN_URL, + search: '', + state: undefined, + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}/`, + url: `${REPORT_IRS_PLAN_URL}/`, + }, plans: fixtures.plans as IRSPlan[], }; shallow( @@ -31,6 +48,19 @@ describe('components/IRS Reports/IRSPlansList', () => { it('renders plan definition list correctly', () => { fetch.mockResponseOnce(JSON.stringify({})); const props = { + history, + location: { + hash: '', + pathname: REPORT_IRS_PLAN_URL, + search: '', + state: undefined, + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}/`, + url: `${REPORT_IRS_PLAN_URL}/`, + }, plans: fixtures.plans as IRSPlan[], }; const wrapper = mount( @@ -44,4 +74,100 @@ describe('components/IRS Reports/IRSPlansList', () => { expect(toJson(wrapper.find('tbody tr td'))).toMatchSnapshot('table rows'); wrapper.unmount(); }); + + it('handles search correctly', async () => { + store.dispatch(fetchIRSPlans(fixtures.plans as IRSPlan[])); + + const props = { + fetchPlans: jest.fn(), + history, + location: { + pathname: REPORT_IRS_PLAN_URL, + search: '?title=Berg', + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}`, + url: `${REPORT_IRS_PLAN_URL}`, + }, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('tbody tr td') + .find('Link') + .at(0) + .text() + ).toEqual('Berg Namibia 2019'); + }); + + it('handles a case insensitive search', async () => { + store.dispatch(fetchIRSPlans(fixtures.plans as IRSPlan[])); + + const props = { + fetchPlans: jest.fn(), + history, + location: { + pathname: REPORT_IRS_PLAN_URL, + search: '?title=BERG', + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}`, + url: `${REPORT_IRS_PLAN_URL}`, + }, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('tbody tr td') + .find('Link') + .at(0) + .text() + ).toEqual('Berg Namibia 2019'); + }); + + it('renders empty table if no search matches', async () => { + store.dispatch(fetchIRSPlans(fixtures.plans as IRSPlan[])); + + const props = { + fetchPlans: jest.fn(), + history, + location: { + pathname: REPORT_IRS_PLAN_URL, + search: '?title=Amazon', + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}`, + url: `${REPORT_IRS_PLAN_URL}`, + }, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + expect(toJson(wrapper.find('tbody tr'))).toEqual(null); + }); }); diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx index 31587ac9a5..0b91e9e167 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -3,9 +3,11 @@ import reducerRegistry from '@onaio/redux-reducer-registry'; import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router'; import { Link } from 'react-router-dom'; import { Col, Row } from 'reactstrap'; import { Store } from 'redux'; +import { SearchForm } from '../../../../components/forms/Search'; import LinkAsButton from '../../../../components/LinkAsButton'; import HeaderBreadcrumb from '../../../../components/page/HeaderBreadcrumb/HeaderBreadcrumb'; import Loading from '../../../../components/page/Loading'; @@ -19,15 +21,21 @@ import { TITLE, } from '../../../../configs/lang'; import { PlanDefinition, planStatusDisplay } from '../../../../configs/settings'; -import { HOME_URL, OPENSRP_PLANS, PLAN_LIST_URL, PLAN_UPDATE_URL } from '../../../../constants'; +import { + HOME_URL, + OPENSRP_PLANS, + PLAN_LIST_URL, + PLAN_UPDATE_URL, + QUERY_PARAM_TITLE, +} from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; +import { getQueryParams } from '../../../../helpers/utils'; import { OpenSRPService } from '../../../../services/opensrp'; import planDefinitionReducer, { fetchPlanDefinitions, - getPlanDefinitionsArray, + makePlanDefinitionsArraySelector, reducerName as planDefinitionReducerName, } from '../../../../store/ducks/opensrp/PlanDefinition'; - /** register the plan definitions reducer */ reducerRegistry.register(planDefinitionReducerName, planDefinitionReducer); @@ -39,14 +47,11 @@ interface PlanListProps { } /** Simple component that loads the new plan form and allows you to create a new plan */ -const PlanDefinitionList = (props: PlanListProps) => { +const PlanDefinitionList = (props: PlanListProps & RouteComponentProps) => { const { fetchPlans, plans, service } = props; const [loading, setLoading] = useState(true); - const apiService = new service(OPENSRP_PLANS); - const pageTitle: string = PLANS; - const breadcrumbProps = { currentPage: { label: pageTitle, @@ -77,12 +82,8 @@ const PlanDefinitionList = (props: PlanListProps) => { loadData().catch(err => displayError(err)); }, []); - if (loading === true) { - return ; - } - - const listViewProps = { - data: plans.map(planObj => { + const listViewData = (data: PlanDefinition[]) => + data.map(planObj => { const typeUseContext = planObj.useContext.filter(e => e.code === 'interventionType'); return [ @@ -93,7 +94,14 @@ const PlanDefinitionList = (props: PlanListProps) => { planStatusDisplay[planObj.status] || planObj.status, planObj.date, ]; - }), + }); + + if (loading === true) { + return ; + } + + const listViewProps = { + data: listViewData(plans), headerItems: [TITLE, INTERVENTION_TYPE_LABEL, STATUS_HEADER, LAST_MODIFIED], tableClass: 'table table-bordered plans-list', }; @@ -115,6 +123,8 @@ const PlanDefinitionList = (props: PlanListProps) => { /> +
+ @@ -143,8 +153,11 @@ interface DispatchedStateProps { } /** map state to props */ -const mapStateToProps = (state: Partial): DispatchedStateProps => { - const planDefinitionsArray = getPlanDefinitionsArray(state); +const mapStateToProps = (state: Partial, ownProps: any): DispatchedStateProps => { + const searchedTitle = getQueryParams(ownProps.location)[QUERY_PARAM_TITLE] as string; + const planDefinitionsArray = makePlanDefinitionsArraySelector()(state, { + title: searchedTitle, + }); return { plans: planDefinitionsArray, diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/__snapshots__/index.test.tsx.snap b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/__snapshots__/index.test.tsx.snap index 817c3db16c..0384e096ff 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/__snapshots__/index.test.tsx.snap +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/__snapshots__/index.test.tsx.snap @@ -1,1488 +1,215 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly 1`] = ` - - =", - "unit": "form(s)", - "value": 1, - }, - }, - "due": "2019-05-21", - "measure": "Number of case confirmation forms complete", - }, - ], - }, - Object { - "description": "Register all families and family members in all residential structures enumerated or added (100%) within operational area", - "id": "RACD_register_all_families", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "Percent", - "value": 100, - }, - }, - "due": "2019-08-30", - "measure": "Percent of residential structures with full family registration", - }, - ], - }, - Object { - "description": "Visit 100% of residential structures in the operational area and provide nets", - "id": "RACD_bednet_dist_1km_radius", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "Percent", - "value": 100, - }, - }, - "due": "2019-08-30", - "measure": "Percent of residential structures received nets", - }, - ], - }, - Object { - "description": "Visit all residential structures (100%) within a 1 km radius of a confirmed index case and test each registered person", - "id": "RACD_blood_screening_1km_radius", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "Percent", - "value": 100, - }, - }, - "due": "2019-05-28", - "measure": "Percent of registered people tested", - }, - ], - }, - Object { - "description": "Perform a minimum of three larval dipping activities in the operational area", - "id": "Larval_Dipping_Min_3_Sites", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "form(s)", - "value": 3, - }, - }, - "due": "2019-05-28", - "measure": "Number of larval dipping forms submitted", - }, - ], - }, - Object { - "description": "Set a minimum of three mosquito collection traps and complete the mosquito collection process", - "id": "Mosquito_Collection_Min_3_Traps", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "form(s)", - "value": 3, - }, - }, - "due": "2019-05-28", - "measure": "Number of mosquito collection forms submitted", - }, - ], - }, - Object { - "description": "Complete at least 1 BCC activity for the operational area", - "id": "BCC_Focus", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "form(s)", - "value": 1, - }, - }, - "due": "2019-06-21", - "measure": "Number of BCC forms submitted", - }, - ], - }, - ], - "identifier": "356b6b84-fc36-4389-a44a-2b038ed2f38d", - "jurisdiction": Array [ - Object { - "code": "3952", - }, - ], - "name": "A2-Lusaka_Akros_Focus_2", - "serverVersion": 1563303150422, - "status": "active", - "title": "A2-Lusaka Akros Test Focus 2", - "useContext": Array [ - Object { - "code": "interventionType", - "valueCodableConcept": "FI", - }, - Object { - "code": "fiStatus", - "valueCodableConcept": "A2", - }, - Object { - "code": "fiReason", - "valueCodableConcept": "Routine", - }, - ], - "version": "1", - }, - Object { - "action": Array [ - Object { - "code": "BCC", - "description": "Perform BCC for the operational area", - "goalId": "BCC_complete", - "identifier": "3f2eef38-38fe-4108-abb3-4e896b880302", - "prefix": 1, - "reason": "Routine", - "subjectCodableConcept": Object { - "text": "Operational_Area", - }, - "taskTemplate": "Action1_Perform_BCC", - "timingPeriod": Object { - "end": "2019-07-30", - "start": "2019-07-10", - }, - "title": "Perform BCC", - }, - Object { - "code": "IRS", - "description": "Visit each structure in the operational area and attempt to spray", - "goalId": "90_percent_of_structures_sprayed", - "identifier": "95a5a00f-a411-4fe5-bd9c-460a856239c9", - "prefix": 2, - "reason": "Routine", - "subjectCodableConcept": Object { - "text": "Residential_Structure", - }, - "taskTemplate": "Action2_Spray_Structures", - "timingPeriod": Object { - "end": "2019-07-30", - "start": "2019-07-10", - }, - "title": "Spray Structures", - }, - ], - "date": "2019-07-10", - "effectivePeriod": Object { - "end": "2019-07-30", - "start": "2019-07-10", - }, - "goal": Array [ - Object { - "description": "Complete BCC for the operational area", - "id": "BCC_complete", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "each", - "value": 1, - }, - }, - "due": "2019-07-10", - "measure": "Number of BCC communication activities that happened", - }, - ], - }, - Object { - "description": "Spray 90 % of structures in the operational area", - "id": "90_percent_of_structures_sprayed", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "percent", - "value": 90, - }, - }, - "due": "2019-07-30", - "measure": "Percent of structures sprayed", - }, - ], - }, - ], - "identifier": "8fa7eb32-99d7-4b49-8332-9ecedd6d51ae", - "jurisdiction": Array [ - Object { - "code": "35968df5-f335-44ae-8ae5-25804caa2d86", - }, - Object { - "code": "3952", - }, - Object { - "code": "ac7ba751-35e8-4b46-9e53-3cbaad193697", - }, - ], - "name": "TwoTwoOne_01_IRS_2019-07-10", - "serverVersion": 1563303151426, - "status": "active", - "title": "TwoTwoOne_01 IRS 2019-07-10", - "useContext": Array [ - Object { - "code": "interventionType", - "valueCodableConcept": "IRS", - }, - ], - "version": "1", - }, - Object { - "action": Array [ - Object { - "code": "Case Confirmation", - "description": "Confirm the index case", - "goalId": "Case_Confirmation", - "identifier": "031f3d7f-e222-459e-9fcc-da63d04dba4b", - "prefix": 1, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Operational Area", - }, - "taskTemplate": "Case_Confirmation", - "timingPeriod": Object { - "end": "2019-07-28", - "start": "2019-07-18", - }, - "title": "Case Confirmation", - }, - Object { - "code": "RACD Register Family", - "description": "Register all families u0026 family members in all residential structures enumerated (100%) within the operational area", - "goalId": "RACD_register_all_families", - "identifier": "6729612a-9e83-4d72-a8c6-da518e530190", - "prefix": 2, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Residential_Structure", - }, - "taskTemplate": "RACD_register_families", - "timingPeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "title": "Family Registration", - }, - Object { - "code": "Blood Screening", - "description": "Visit all residential structures (100%) within a 1 km radius of a confirmed index case and test each registered person", - "goalId": "RACD_blood_screening_1km_radius", - "identifier": "6c3637dd-b36d-4137-8df2-1efc6d8d858f", - "prefix": 3, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Person", - }, - "taskTemplate": "RACD_Blood_Screening", - "timingPeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "title": "Blood screening", - }, - Object { - "code": "Bednet Distribution", - "description": "Visit 100% of residential structures in the operational area and provide nets", - "goalId": "RACD_bednet_dist_1km_radius", - "identifier": "eb85377d-9333-407c-88de-155410fbfd88", - "prefix": 4, - "reason": "Routine", - "subjectCodableConcept": Object { - "text": "Residential_Structure", - }, - "taskTemplate": "ITN_Visit_Structures", - "timingPeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "title": "Bednet Distribution", - }, - Object { - "code": "Larval Dipping", - "description": "Perform a minimum of three larval dipping activities in the operational area", - "goalId": "Larval_Dipping_Min_3_Sites", - "identifier": "01369e56-7e72-4b2e-9f4e-9e3c2beda706", - "prefix": 5, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Breeding_Site", - }, - "taskTemplate": "Larval_Dipping", - "timingPeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "title": "Larval Dipping", - }, - Object { - "code": "Mosquito Collection", - "description": "Set a minimum of three mosquito collection traps and complete the mosquito collection process", - "goalId": "Mosquito_Collection_Min_3_Traps", - "identifier": "6bd72f5b-1043-4905-bcf2-ef62c34f606f", - "prefix": 6, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Mosquito_Collection_Point", - }, - "taskTemplate": "Mosquito_Collection_Point", - "timingPeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "title": "Mosquito Collection", - }, - Object { - "code": "BCC", - "description": "Conduct BCC activity", - "goalId": "BCC_Focus", - "identifier": "40b046a5-18bd-4f26-b3aa-87a0d3787377", - "prefix": 7, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Operational_Area", - }, - "taskTemplate": "BCC_Focus", - "timingPeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "title": "Behaviour Change Communication", - }, - ], - "date": "2019-07-18", - "effectivePeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", - }, - "goal": Array [ - Object { - "description": "Confirm the index case", - "id": "Case_Confirmation", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "form(s)", - "value": 1, - }, - }, - "due": "2019-07-28", - "measure": "Number of case confirmation forms complete", - }, - ], - }, - Object { - "description": "Register all families and family members in all residential structures enumerated or added (100%) within operational area", - "id": "RACD_register_all_families", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "Percent", - "value": 100, - }, - }, - "due": "2019-08-07", - "measure": "Percent of residential structures with full family registration", - }, - ], - }, - Object { - "description": "Visit all residential structures (100%) within a 1 km radius of a confirmed index case and test each registered person", - "id": "RACD_blood_screening_1km_radius", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "person(s)", - "value": 50, - }, - }, - "due": "2019-08-07", - "measure": "Number of registered people tested", - }, - ], - }, - Object { - "description": "Visit 100% of residential structures in the operational area and provide nets", - "id": "RACD_bednet_dist_1km_radius", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "Percent", - "value": 90, - }, - }, - "due": "2019-08-07", - "measure": "Percent of residential structures received nets", - }, - ], - }, - Object { - "description": "Perform a minimum of three larval dipping activities in the operational area", - "id": "Larval_Dipping_Min_3_Sites", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "form(s)", - "value": 3, - }, - }, - "due": "2019-08-07", - "measure": "Number of larval dipping forms submitted", - }, - ], - }, - Object { - "description": "Set a minimum of three mosquito collection traps and complete the mosquito collection process", - "id": "Mosquito_Collection_Min_3_Traps", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "form(s)", - "value": 3, - }, - }, - "due": "2019-08-07", - "measure": "Number of mosquito collection forms submitted", - }, - ], - }, - Object { - "description": "Complete at least 1 BCC activity for the operational area", - "id": "BCC_Focus", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": "u003eu003d", - "unit": "form(s)", - "value": 1, - }, - }, - "due": "2019-08-07", - "measure": "Number of BCC forms submitted", - }, - ], - }, - ], - "identifier": "f0558ad1-396d-4d97-9fff-c46cf92b6ce6", - "jurisdiction": Array [ - Object { - "code": "ac7ba751-35e8-4b46-9e53-3cbaad193697", - }, - ], - "name": "A1-TwoTwoTwo_03-2019-07-18", - "serverVersion": 1563494230144, - "status": "active", - "title": "A1 - TwoTwoTwo_03 - 2019-07-18", - "useContext": Array [ - Object { - "code": "interventionType", - "valueCodableConcept": "FI", - }, - Object { - "code": "fiStatus", - "valueCodableConcept": "A1", - }, - Object { - "code": "fiReason", - "valueCodableConcept": "Case Triggered", - }, - Object { - "code": "caseNum", - "valueCodableConcept": "1", - }, - Object { - "code": "opensrpEventId", - "valueCodableConcept": "1", - }, - Object { - "code": "taskGenerationStatus", - "valueCodableConcept": "True", - }, - ], - "version": "1", - }, - Object { - "action": Array [], - "date": "2019-05-19", - "effectivePeriod": Object { - "end": "2019-08-30", - "start": "2019-05-20", - }, - "goal": Array [], - "identifier": "0e85c238-39c1-4cea-a926-3d89f0c98429", - "jurisdiction": Array [ - Object { - "code": "3952", - }, - ], - "name": "mosh-test", - "serverVersion": 1563807467576, - "status": "draft", - "title": "A Test By Mosh", - "useContext": Array [ - Object { - "code": "interventionType", - "valueCodableConcept": "FI", - }, - Object { - "code": "fiStatus", - "valueCodableConcept": "A2", - }, - Object { - "code": "fiReason", - "valueCodableConcept": "Routine", - }, - ], - }, - Object { - "action": Array [ - Object { - "code": "RACD Register Family", - "description": "Register all families & family members in all residential structures enumerated (100%) within the operational area", - "goalId": "RACD_register_all_families", - "identifier": "2df855c3-3179-41f1-b8d8-7f84de7fa684", - "prefix": 1, - "reason": "Investigation", - "subjectCodableConcept": Object { - "text": "Residential_Structure", - }, - "taskTemplate": "RACD_register_families", - "timingPeriod": Object { - "end": "2019-12-31", - "start": "2019-10-18", - }, - "title": "Family Registration", - }, - Object { - "code": "MDA Dispense", - "description": "Visit all residential structures (100%) and dispense prophylaxis to each registered person", - "goalId": "MDA_Dispense", - "identifier": "da253449-8a89-46ea-8cdf-d4159240edae", - "prefix": 2, - "reason": "Routine", - "subjectCodableConcept": Object { - "text": "Person", - }, - "taskTemplate": "MDA_Dispense", - "timingPeriod": Object { - "end": "2019-12-31", - "start": "2019-10-18", - }, - "title": "MDA Round 1 Dispense", - }, - Object { - "code": "MDA Adherence", - "description": "Visit all residential structures (100%) and confirm adherence of each registered person", - "goalId": "MDA_Dispense", - "identifier": "1c688154-e025-4a40-91fd-9baed25a0ba4", - "prefix": 3, - "reason": "Routine", - "subjectCodableConcept": Object { - "text": "Person", - }, - "taskTemplate": "MDA_Adherence", - "timingPeriod": Object { - "end": "2019-12-31", - "start": "2019-10-18", - }, - "title": "MDA Round 1 Adherence", - }, - ], - "date": "2019-10-18", - "effectivePeriod": Object { - "end": "2019-12-31", - "start": "2019-10-18", - }, - "goal": Array [ - Object { - "description": "Register all families and family members in all residential structures enumerated or added (100%) within operational area", - "id": "RACD_register_all_families", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "Percent", - "value": 100, - }, - }, - "due": "2019-12-31", - "measure": "Percent of residential structures with full family registration", - }, - ], - }, - Object { - "description": "Visit all residential structures (100%) dispense prophylaxis to each registered person", - "id": "MDA_Dispense", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "percent", - "value": 100, - }, - }, - "due": "2019-12-31", - "measure": "Percent of Registered person(s)", - }, - ], - }, - Object { - "description": "Visit all residential structures (100%) and confirm adherence of each registered person", - "id": "MDA_Adherence", - "priority": "medium-priority", - "target": Array [ - Object { - "detail": Object { - "detailQuantity": Object { - "comparator": ">=", - "unit": "percent", - "value": 100, - }, - }, - "due": "2019-12-31", - "measure": "Percent of dispense recipients", - }, - ], - }, - ], - "identifier": "f3da140c-2d2c-4bf7-8189-c2349f143a72", - "jurisdiction": Array [ - Object { - "code": "3951", - }, - Object { - "code": "3952", - }, - ], - "name": "Macepa-MDA-Campaign-2019-10-18", - "serverVersion": 1571406689603, - "status": "active", - "title": "Macepa MDA Campaign", - "useContext": Array [ - Object { - "code": "interventionType", - "valueCodableConcept": "MDA", - }, - Object { - "code": "taskGenerationStatus", - "valueCodableConcept": "True", - }, - ], - "version": "1", - }, - ] - } - service={[Function]} + -
- + + +`; + +exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: list view props 1`] = ` +Object { + "data": Array [ + Array [ + - - - - - , + "FI", + "active", + "2019-05-19", + ], + Array [ + -
- - - -
-
- , + "IRS", + "active", + "2019-07-10", + ], + Array [ + + A1 - TwoTwoTwo_03 - 2019-07-18 + , + "FI", + "active", + "2019-07-18", + ], + Array [ + + A Test By Mosh + , + "FI", + "draft", + "2019-05-19", + ], + Array [ + + Macepa MDA Campaign + , + "MDA", + "active", + "2019-10-18", + ], + ], + "headerItems": Array [ + "Title", + "Intervention Type", + "Status", + "Last Modified", + ], + "renderHeaders": [Function], + "renderRows": [Function], + "tableClass": "table table-bordered plans-list", + "tbodyClass": "listview-tbody", + "tdClass": "listview-td", + "thClass": "listview-th", + "theadClass": "listview-thead", + "trClass": "listview-tr", +} +`; + +exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: row heading 1`] = ` + +
+ +
-
- -
-

- Manage Plans -

-
- - -
- - - - Add Plan - - - -
- -
- - +
+ + +
-
- -
- - A2-Lusaka Akros Test Focus 2 - , - "FI", - "active", - "2019-05-19", - ], - Array [ - - TwoTwoOne_01 IRS 2019-07-10 - , - "IRS", - "active", - "2019-07-10", - ], - Array [ - - A1 - TwoTwoTwo_03 - 2019-07-18 - , - "FI", - "active", - "2019-07-18", - ], - Array [ - - A Test By Mosh - , - "FI", - "draft", - "2019-05-19", - ], - Array [ - - Macepa MDA Campaign - , - "MDA", - "active", - "2019-10-18", - ], - ] - } - headerItems={ - Array [ - "Title", - "Intervention Type", - "Status", - "Last Modified", - ] - } - renderHeaders={[Function]} - renderRows={[Function]} - tableClass="table table-bordered plans-list" - tbodyClass="listview-tbody" - tdClass="listview-td" - thClass="listview-th" - theadClass="listview-thead" - trClass="listview-tr" - > - - - - - - - - - - - - - - - A2-Lusaka Akros Test Focus 2 - , - "FI", - "active", - "2019-05-19", - ] - } - > - - - - - - - - - TwoTwoOne_01 IRS 2019-07-10 - , - "IRS", - "active", - "2019-07-10", - ] - } - > - - - - - - - - - A1 - TwoTwoTwo_03 - 2019-07-18 - , - "FI", - "active", - "2019-07-18", - ] - } - > - - - - - - - - - A Test By Mosh - , - "FI", - "draft", - "2019-05-19", - ] - } - > - - - - - - - - - Macepa MDA Campaign - , - "MDA", - "active", - "2019-10-18", - ] - } - > - - - - - - - -
- Title - - Intervention Type - - Status - - Last Modified -
- - - A2-Lusaka Akros Test Focus 2 - - - - FI - - active - - 2019-05-19 -
- - - TwoTwoOne_01 IRS 2019-07-10 - - - - IRS - - active - - 2019-07-10 -
- - - A1 - TwoTwoTwo_03 - 2019-07-18 - - - - FI - - active - - 2019-07-18 -
- - - A Test By Mosh - - - - FI - - draft - - 2019-05-19 -
- - - Macepa MDA Campaign - - - - MDA - - active - - 2019-10-18 -
-
-
- -
- -
- - + Add Plan + + + +
+ +
+
+`; + +exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: search form props 1`] = ` +Object { + "history": Object { + "action": "POP", + "block": [Function], + "createHref": [Function], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "key", + "pathname": "/plans/list", + "search": "", + "state": undefined, + }, + "placeholder": "Search", +} `; diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index c8ab3963a1..cac8a9be4a 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -2,8 +2,12 @@ import { mount, shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { createBrowserHistory } from 'history'; import React from 'react'; +import { Provider } from 'react-redux'; import { Router } from 'react-router'; -import { PlanDefinitionList } from '../'; +import ConnectedPlanDefinitionList, { PlanDefinitionList } from '../'; +import { PLAN_LIST_URL } from '../../../../../constants'; +import store from '../../../../../store'; +import { fetchPlanDefinitions } from '../../../../../store/ducks/opensrp/PlanDefinition'; import * as fixtures from '../../../../../store/ducks/opensrp/PlanDefinition/tests/fixtures'; /* tslint:disable-next-line no-var-requires */ @@ -16,8 +20,25 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { jest.resetAllMocks(); }); + beforeAll(() => { + global.Date.now = jest.fn(() => new Date('2019-04-07T10:20:30Z').getTime()); + }); + it('renders without crashing', () => { const props = { + history, + location: { + hash: '', + pathname: PLAN_LIST_URL, + search: '', + state: undefined, + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}/`, + url: `${PLAN_LIST_URL}/`, + }, plans: fixtures.plans, }; shallow( @@ -30,6 +51,20 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { it('renders plan definition list correctly', () => { fetch.mockResponseOnce(fixtures.plansJSON); const props = { + history, + location: { + hash: '', + key: 'key', + pathname: PLAN_LIST_URL, + search: '', + state: undefined, + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}/`, + url: `${PLAN_LIST_URL}/`, + }, plans: fixtures.plans, }; const wrapper = mount( @@ -37,7 +72,110 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(wrapper.find('HeaderBreadcrumb').props()).toMatchSnapshot('bread crumb props'); + expect(toJson(wrapper.find('Row').at(0))).toMatchSnapshot('row heading'); + expect(wrapper.find('SearchForm').props()).toMatchSnapshot('search form props'); + expect(wrapper.find('ListView').props()).toMatchSnapshot('list view props'); + expect(toJson(wrapper.find('HelmetWrapper'))).toMatchSnapshot('helmet'); wrapper.unmount(); }); + + it('handles search correctly', async () => { + store.dispatch(fetchPlanDefinitions(fixtures.plans)); + + const props = { + fetchPlans: jest.fn(), + history, + location: { + pathname: PLAN_LIST_URL, + search: '?title=Mosh', + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}`, + url: `${PLAN_LIST_URL}`, + }, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + wrapper.mount(); + expect( + wrapper + .find('tbody tr td') + .find('Link') + .at(0) + .text() + ).toEqual('A Test By Mosh'); + }); + + it('handles a case insensitive search', async () => { + store.dispatch(fetchPlanDefinitions(fixtures.plans)); + + const props = { + fetchPlans: jest.fn(), + history, + location: { + pathname: PLAN_LIST_URL, + search: '?title=MOSH', + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}`, + url: `${PLAN_LIST_URL}`, + }, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + wrapper.mount(); + expect( + wrapper + .find('tbody tr td') + .find('Link') + .at(0) + .text() + ).toEqual('A Test By Mosh'); + }); + + it('renders empty table if no search matches', async () => { + store.dispatch(fetchPlanDefinitions(fixtures.plans)); + + const props = { + fetchPlans: jest.fn(), + history, + location: { + pathname: PLAN_LIST_URL, + search: '?title=Amazon', + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}`, + url: `${PLAN_LIST_URL}`, + }, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + + + ); + wrapper.mount(); + expect(toJson(wrapper.find('tbody tr'))).toEqual(null); + }); }); diff --git a/src/helpers/tests/utils.test.tsx b/src/helpers/tests/utils.test.tsx index d0770e0b66..0e8a4dc778 100644 --- a/src/helpers/tests/utils.test.tsx +++ b/src/helpers/tests/utils.test.tsx @@ -36,6 +36,7 @@ import { getColor, getColorByValue, getLocationColumns, + getQueryParams, IndicatorThresholdItemPercentage, isPlanDefinitionOfType, oAuthUserInfoGetter, @@ -359,4 +360,17 @@ describe('helpers/utils', () => { const result = IndicatorThresholdItemPercentage(Item, 1); expect(result).toEqual('60.0%'); }); + + it('gets query params from URL correctly', () => { + const location = { + hash: '', + pathname: '/foo', + query: {}, + search: '?q=venom', + state: undefined, + }; + expect(getQueryParams(location)).toEqual({ + q: 'venom', + }); + }); }); diff --git a/src/helpers/utils.tsx b/src/helpers/utils.tsx index b3846a9052..1f6ed43951 100644 --- a/src/helpers/utils.tsx +++ b/src/helpers/utils.tsx @@ -4,8 +4,10 @@ import { SessionState } from '@onaio/session-reducer'; import { Dictionary, percentage } from '@onaio/utils'; import { Color } from 'csstype'; import { GisidaMap } from 'gisida'; -import { findKey, uniq } from 'lodash'; +import { Location } from 'history'; +import { findKey, trimStart, uniq } from 'lodash'; import { FitBoundsOptions, Layer, LngLatBoundsLike, LngLatLike, Map, Style } from 'mapbox-gl'; +import querystring from 'querystring'; import { MouseEvent } from 'react'; import React from 'react'; import { Link } from 'react-router-dom'; @@ -871,3 +873,11 @@ export const IndicatorThresholdItemPercentage = (item: number, decimalPoints?: n }; export const reactSelectNoOptionsText = () => NO_OPTIONS; + +/** + * Get query params from URL + * @param {Location} location from props + */ +export const getQueryParams = (location: Location) => { + return querystring.parse(trimStart(location.search, '?')); +}; diff --git a/src/store/ducks/generic/plans.ts b/src/store/ducks/generic/plans.ts index 6df4162fb5..b648071884 100644 --- a/src/store/ducks/generic/plans.ts +++ b/src/store/ducks/generic/plans.ts @@ -1,5 +1,7 @@ +import intersect from 'fast_array_intersect'; import { get, keyBy, values } from 'lodash'; import { AnyAction, Store } from 'redux'; +import { createSelector } from 'reselect'; import SeamlessImmutable from 'seamless-immutable'; import { InterventionType, PlanStatus } from '../plans'; @@ -172,3 +174,57 @@ export function getIRSPlansArray( const result = values((state as any)[reducerName].IRSPlansById); return result.filter((e: IRSPlan) => e.plan_intervention_type === interventionType); } + +/** RESELECT USAGE STARTS HERE */ + +/** This interface represents the structure of IRS plan filter options/params */ +export interface IRSPlanFilters { + plan_title?: string /** IRS plan title */; +} + +/** IRSPlansArrayBaseSelector select an array of all plans + * @param state - the redux store + */ +export const IRSPlansArrayBaseSelector = (planKey?: string) => (state: Partial): IRSPlan[] => + values((state as any)[reducerName][planKey ? planKey : 'IRSPlansById']); + +/** getIRSPlansArrayByTitle + * Gets title from PlanFilters + * @param state - the redux store + * @param props - the plan filters object + */ +export const getTitle = (_: Partial, props: IRSPlanFilters) => props.plan_title; + +/** getPlansArrayByTitle + * Gets an array of Plan objects filtered by plan title + * @param {Partial} state - the redux store + * @param {PlanDefinitionFilters} props - the plan defintion filters object + */ +export const getIRSPlansArrayByTitle = (planKey?: string) => + createSelector([IRSPlansArrayBaseSelector(planKey), getTitle], (plans, title) => + title + ? plans.filter(plan => plan.plan_title.toLowerCase().includes(title.toLowerCase())) + : plans + ); + +/** makeIRSPlansArraySelector + * Returns a selector that gets an array of IRSPlan objects filtered by one or all + * of the following: + * - plan_title + * + * These filter params are all optional and are supplied via the prop parameter. + * + * This selector is meant to be a memoized replacement for getIRSPlansArray. + * + * To use this selector, do something like: + * const IRSPlansArraySelector = makeIRSPlansArraySelector(); + * + * @param {Partial} state - the redux store + * @param {PlanFilters} props - the plan filters object + * @param {string} sortField - sort by field + */ +export const makeIRSPlansArraySelector = (planKey?: string) => { + return createSelector([getIRSPlansArrayByTitle(planKey)], plans => + intersect([plans], JSON.stringify) + ); +}; diff --git a/src/store/ducks/generic/tests/plans.test.ts b/src/store/ducks/generic/tests/plans.test.ts index 4dfad2d146..05190d39ac 100644 --- a/src/store/ducks/generic/tests/plans.test.ts +++ b/src/store/ducks/generic/tests/plans.test.ts @@ -8,8 +8,10 @@ import reducer, { fetchIRSPlans, getIRSPlanById, getIRSPlansArray, + getIRSPlansArrayByTitle, getIRSPlansById, IRSPlan, + makeIRSPlansArraySelector, reducerName, removeIRSPlans, } from '../plans'; @@ -56,6 +58,22 @@ describe('reducers/IRS/IRSPlan', () => { keyBy([fixtures.plans[0], fixtures.plans[1], fixtures.plans[2]], 'plan_id') ); + // RESELECT TESTS + const titleFilter = { + plan_title: 'Berg', + }; + const titleUpperFilter = { + plan_title: 'BERG', + }; + const IRSPlansArraySelector = makeIRSPlansArraySelector(); + expect(getIRSPlansArrayByTitle()(store.getState(), titleFilter)).toEqual([fixtures.plans[2]]); + expect(getIRSPlansArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([ + fixtures.plans[2], + ]); + expect(IRSPlansArraySelector(store.getState(), { plan_title: 'Berg' })).toEqual([ + fixtures.plans[2], + ]); + // reset store.dispatch(removeIRSPlans()); expect(getIRSPlansArray(store.getState())).toEqual([]); diff --git a/src/store/ducks/opensrp/PlanDefinition/index.ts b/src/store/ducks/opensrp/PlanDefinition/index.ts index 0acf63ce1c..1a4145fd6a 100644 --- a/src/store/ducks/opensrp/PlanDefinition/index.ts +++ b/src/store/ducks/opensrp/PlanDefinition/index.ts @@ -1,5 +1,7 @@ +import intersect from 'fast_array_intersect'; import { get, keyBy, values } from 'lodash'; import { AnyAction, Store } from 'redux'; +import { createSelector } from 'reselect'; import SeamlessImmutable from 'seamless-immutable'; import { PlanDefinition } from '../../../../configs/settings'; import { isPlanDefinitionOfType } from '../../../../helpers/utils'; @@ -166,3 +168,57 @@ export function getPlanDefinitionsArray( } return result; } + +/** RESELECT USAGE STARTS HERE */ + +/** This interface represents the structure of plan definition filter options/params */ +export interface PlanDefinitionFilters { + title?: string /** plan object title */; +} + +/** planDefinitionsArrayBaseSelector select an array of all plans + * @param state - the redux store + */ +export const planDefinitionsArrayBaseSelector = (planKey?: string) => ( + state: Partial +): PlanDefinition[] => + values((state as any)[reducerName][planKey ? planKey : 'planDefinitionsById']); + +/** getPlanDefinitionsArrayByTitle + * Gets title from PlanFilters + * @param state - the redux store + * @param props - the plan filters object + */ +export const getTitle = (_: Partial, props: PlanDefinitionFilters) => props.title; + +/** getPlansArrayByTitle + * Gets an array of Plan objects filtered by plan title + * @param {Registry} state - the redux store + * @param {PlanDefinitionFilters} props - the plan defintion filters object + */ +export const getPlanDefinitionsArrayByTitle = (planKey?: string) => + createSelector([planDefinitionsArrayBaseSelector(planKey), getTitle], (plans, title) => + title ? plans.filter(plan => plan.title.toLowerCase().includes(title.toLowerCase())) : plans + ); + +/** makePlanDefinitionsArraySelector + * Returns a selector that gets an array of IRSPlan objects filtered by one or all + * of the following: + * - title + * + * These filter params are all optional and are supplied via the prop parameter. + * + * This selector is meant to be a memoized replacement for getPlanDefinitionsArray. + * + * To use this selector, do something like: + * const PlanDefinitionsArraySelector = makeIRSPlansArraySelector(); + * + * @param {Partial} state - the redux store + * @param {PlanFilters} props - the plan filters object + * @param {string} sortField - sort by field + */ +export const makePlanDefinitionsArraySelector = (planKey?: string) => { + return createSelector([getPlanDefinitionsArrayByTitle(planKey)], plans => + intersect([plans], JSON.stringify) + ); +}; diff --git a/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts b/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts index dd1fc751e7..e2d325bc7f 100644 --- a/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts +++ b/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts @@ -9,7 +9,9 @@ import reducer, { fetchPlanDefinitions, getPlanDefinitionById, getPlanDefinitionsArray, + getPlanDefinitionsArrayByTitle, getPlanDefinitionsById, + makePlanDefinitionsArraySelector, reducerName, removePlanDefinitions, } from '../index'; @@ -23,6 +25,7 @@ describe('reducers/opensrp/PlanDefinition', () => { beforeEach(() => { flushThunks = FlushThunks.createMiddleware(); jest.resetAllMocks(); + store.dispatch(removePlanDefinitions()); }); it('should have initial state', () => { @@ -60,6 +63,25 @@ describe('reducers/opensrp/PlanDefinition', () => { keyBy([fixtures.plans[0], fixtures.plans[2], fixtures.plans[3]], 'identifier') ); + // RESELECT TESTS + const titleFilter = { + title: 'mosh', + }; + const titleUpperFilter = { + title: 'MOSH', + }; + const PlanDefinitionsArraySelector = makePlanDefinitionsArraySelector(); + + expect(getPlanDefinitionsArrayByTitle()(store.getState(), titleFilter)).toEqual([ + fixtures.plans[3], + ]); + expect(getPlanDefinitionsArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([ + fixtures.plans[3], + ]); + expect(PlanDefinitionsArraySelector(store.getState(), { title: 'Mosh' })).toEqual([ + fixtures.plans[3], + ]); + // reset store.dispatch(removePlanDefinitions()); expect(getPlanDefinitionsArray(store.getState())).toEqual([]); diff --git a/src/store/ducks/plans.ts b/src/store/ducks/plans.ts index f01cf343a7..17d1742502 100644 --- a/src/store/ducks/plans.ts +++ b/src/store/ducks/plans.ts @@ -1,4 +1,3 @@ -import { Registry } from '@onaio/redux-reducer-registry'; import { Dictionary } from '@onaio/utils'; import intersect from 'fast_array_intersect'; import { get, keyBy, keys, pickBy, values } from 'lodash'; @@ -389,12 +388,13 @@ export interface PlanFilters { parentJurisdictionId?: string /** jurisdiction parent id */; reason?: FIReasonType /** plan FI reason */; statusList?: string[] /** array of plan statuses */; + title?: string /** plan title */; } /** plansArrayBaseSelector select an array of all plans * @param state - the redux store */ -export const plansArrayBaseSelector = (planKey?: string) => (state: Registry): Plan[] => +export const plansArrayBaseSelector = (planKey?: string) => (state: Partial): Plan[] => values((state as any)[reducerName][planKey ? planKey : 'plansById']); /** getInterventionType @@ -402,21 +402,22 @@ export const plansArrayBaseSelector = (planKey?: string) => (state: Registry): P * @param state - the redux store * @param props - the plan filters object */ -export const getInterventionType = (_: Registry, props: PlanFilters) => props.interventionType; +export const getInterventionType = (_: Partial, props: PlanFilters) => + props.interventionType; /** getJurisdictionIds * Gets jurisdictionIds from PlanFilters * @param state - the redux store * @param props - the plan filters object */ -export const getJurisdictionIds = (_: Registry, props: PlanFilters) => props.jurisdictionIds; +export const getJurisdictionIds = (_: Partial, props: PlanFilters) => props.jurisdictionIds; /** getParentJurisdictionId * Gets parentJurisdictionId from PlanFilters * @param state - the redux store * @param props - the plan filters object */ -export const getParentJurisdictionId = (_: Registry, props: PlanFilters) => +export const getParentJurisdictionId = (_: Partial, props: PlanFilters) => props.parentJurisdictionId; /** getStatusList @@ -424,18 +425,25 @@ export const getParentJurisdictionId = (_: Registry, props: PlanFilters) => * @param state - the redux store * @param props - the plan filters object */ -export const getStatusList = (_: Registry, props: PlanFilters) => props.statusList; +export const getStatusList = (_: Partial, props: PlanFilters) => props.statusList; /** getReason * Gets reason from PlanFilters * @param state - the redux store * @param props - the plan filters object */ -export const getReason = (_: Registry, props: PlanFilters) => props.reason; +export const getReason = (_: Partial, props: PlanFilters) => props.reason; + +/** getTitle + * Gets title from PlanFilters + * @param state - the redux store + * @param props - the plan filters object + */ +export const getTitle = (_: Partial, props: PlanFilters) => props.title; /** getPlansArrayByInterventionType * Gets an array of Plan objects filtered by interventionType - * @param {Registry} state - the redux store + * @param {Partial} state - the redux store * @param {PlanFilters} props - the plan filters object */ export const getPlansArrayByInterventionType = (planKey?: string) => @@ -449,7 +457,7 @@ export const getPlansArrayByInterventionType = (planKey?: string) => /** getPlansArrayByJurisdictionIds * Gets an array of Plan objects filtered by jurisdictionIds - * @param {Registry} state - the redux store + * @param {Partial} state - the redux store * @param {PlanFilters} props - the plan filters object */ export const getPlansArrayByJurisdictionIds = (planKey?: string) => @@ -463,7 +471,7 @@ export const getPlansArrayByJurisdictionIds = (planKey?: string) => /** getPlansArrayByStatus * Gets an array of Plan objects filtered by plan status - * @param {Registry} state - the redux store + * @param {Partial} state - the redux store * @param {PlanFilters} props - the plan filters object */ export const getPlansArrayByStatus = (plantype?: string) => @@ -475,7 +483,7 @@ export const getPlansArrayByStatus = (plantype?: string) => /** getPlansArrayByReason * Gets an array of Plan objects filtered by FI plan reason - * @param {Registry} state - the redux store + * @param {Partial} state - the redux store * @param {PlanFilters} props - the plan filters object */ export const getPlansArrayByReason = (planKey?: string) => @@ -485,7 +493,7 @@ export const getPlansArrayByReason = (planKey?: string) => /** getPlansArrayByParentJurisdictionId * Gets an array of Plan objects filtered by plan jurisdiction parent_id - * @param {Registry} state - the redux store + * @param {Partial} state - the redux store * @param {PlanFilters} props - the plan filters object */ export const getPlansArrayByParentJurisdictionId = (planKey?: string) => @@ -500,6 +508,19 @@ export const getPlansArrayByParentJurisdictionId = (planKey?: string) => : true) ) ); + +/** getPlansArrayByTitle + * Gets an array of Plan objects filtered by plan title + * @param {Partial} state - the redux store + * @param {PlanFilters} props - the plan filters object + */ +export const getPlansArrayByTitle = (planKey?: string) => + createSelector([plansArrayBaseSelector(planKey), getTitle], (plans, title) => + title + ? plans.filter(plan => plan.plan_title.toLowerCase().includes(title.toLowerCase())) + : plans + ); + /** makePlansArraySelector * Returns a selector that gets an array of Plan objects filtered by one or all * of the following: @@ -508,6 +529,7 @@ export const getPlansArrayByParentJurisdictionId = (planKey?: string) => * - plan status * - FI plan reason * - plan jurisdiction parent_id + * - plan title * * These filter params are all optional and are supplied via the prop parameter. * @@ -516,7 +538,7 @@ export const getPlansArrayByParentJurisdictionId = (planKey?: string) => * To use this selector, do something like: * const plansArraySelector = makePlansArraySelector(); * - * @param {Registry} state - the redux store + * @param {Partial} state - the redux store * @param {PlanFilters} props - the plan filters object * @param {string} sortField - sort by field */ @@ -528,13 +550,14 @@ export const makePlansArraySelector = (planKey?: string, sortField?: string) => getPlansArrayByStatus(planKey), getPlansArrayByReason(planKey), getPlansArrayByParentJurisdictionId(planKey), + getPlansArrayByTitle(planKey), ], - (plans, plans2, plans3, plans4, plans5) => + (plans, plans2, plans3, plans4, plans5, plans6) => sortField ? descendingOrderSort( - intersect([plans, plans2, plans3, plans4, plans5], JSON.stringify), + intersect([plans, plans2, plans3, plans4, plans5, plans6], JSON.stringify), sortField ) - : intersect([plans, plans2, plans3, plans4, plans5], JSON.stringify) + : intersect([plans, plans2, plans3, plans4, plans5, plans6], JSON.stringify) ); }; diff --git a/src/store/ducks/tests/fixtures.ts b/src/store/ducks/tests/fixtures.ts index 6574065648..8695109266 100644 --- a/src/store/ducks/tests/fixtures.ts +++ b/src/store/ducks/tests/fixtures.ts @@ -45,6 +45,66 @@ export const plan2: Plan = { plan_version: '1', }; +export const plan22: Plan = { + id: 'plan-22', + jurisdiction_depth: 2, + jurisdiction_id: '450fc15b-5bd2-468a-927a-49cb10d3bcac', + jurisdiction_name: 'NVI_439', + jurisdiction_name_path: ['Chadiza', 'Naviluli'], + jurisdiction_parent_id: '2944', + jurisdiction_path: ['2939', '2944'], + plan_date: '2019-06-18', + plan_effective_period_end: '2019-06-18', + plan_effective_period_start: '2019-07-31', + plan_fi_reason: 'Routine' as FIReasonType, + plan_fi_status: 'A1' as FIStatusType, + plan_id: '10f9e9fa-ce34-4b27-a961-72fab5206ab6', + plan_intervention_type: InterventionType.FI, + plan_status: PlanStatus.ACTIVE, + plan_title: 'Test by John Doe', + plan_version: '1', +}; + +export const plan24: Plan = { + id: 'plan-24', + jurisdiction_depth: 2, + jurisdiction_id: '450fc15b-5bd2-468a-927a-49cb10d3bcac', + jurisdiction_name: 'NVI_439', + jurisdiction_name_path: ['Chadiza', 'Naviluli'], + jurisdiction_parent_id: '2944', + jurisdiction_path: ['2939', '2944'], + plan_date: '2019-06-18', + plan_effective_period_end: '2019-06-18', + plan_effective_period_start: '2019-07-31', + plan_fi_reason: 'Case Triggered' as FIReasonType, + plan_fi_status: 'A1' as FIStatusType, + plan_id: '10f9e9fa-ce34-4b27-a961-72fab5206ab6', + plan_intervention_type: InterventionType.FI, + plan_status: PlanStatus.ACTIVE, + plan_title: 'Test by John Doe', + plan_version: '1', +}; + +export const plan25: Plan = { + id: 'plan-25', + jurisdiction_depth: 2, + jurisdiction_id: '450fc15b-5bd2-468a-927a-49cb10d3bcac', + jurisdiction_name: 'NVI_439', + jurisdiction_name_path: ['Chadiza', 'Naviluli'], + jurisdiction_parent_id: '2944', + jurisdiction_path: ['2939', '2944'], + plan_date: '2019-06-18', + plan_effective_period_end: '2019-06-18', + plan_effective_period_start: '2019-07-31', + plan_fi_reason: 'Case Triggered' as FIReasonType, + plan_fi_status: 'A1' as FIStatusType, + plan_id: '10f9e9fa-ce34-4b27-a961-72fab5206ab6', + plan_intervention_type: InterventionType.FI, + plan_status: PlanStatus.ACTIVE, + plan_title: 'Test by Jane Doe', + plan_version: '1', +}; + export const draftPlan = { id: 'draftPlan-id-2', jurisdiction_depth: 41, diff --git a/src/store/ducks/tests/plans.test.ts b/src/store/ducks/tests/plans.test.ts index d03a8b376a..5f63d4c261 100644 --- a/src/store/ducks/tests/plans.test.ts +++ b/src/store/ducks/tests/plans.test.ts @@ -17,6 +17,7 @@ import reducer, { getPlansArrayByParentJurisdictionId, getPlansArrayByReason, getPlansArrayByStatus, + getPlansArrayByTitle, getPlansById, getPlansIdArray, InterventionType, @@ -39,6 +40,7 @@ describe('reducers/plans', () => { beforeEach(() => { flushThunks = FlushThunks.createMiddleware(); jest.resetAllMocks(); + store.dispatch(removePlansAction); }); it('should have initial state', () => { @@ -56,11 +58,11 @@ describe('reducers/plans', () => { }); it('should fetch Plans', () => { - store.dispatch(fetchPlans(fixtures.plans)); + store.dispatch(fetchPlans([...fixtures.plans, fixtures.plan22])); const plansArraySelector = makePlansArraySelector(); - const allPlans = keyBy(fixtures.plans, (plan: Plan) => plan.id); + const allPlans = keyBy([...fixtures.plans, fixtures.plan22], (plan: Plan) => plan.id); const fiPlans = pickBy(allPlans, (e: Plan) => e.plan_intervention_type === InterventionType.FI); const irsPlans = pickBy( allPlans, @@ -87,6 +89,7 @@ describe('reducers/plans', () => { expect(getPlansIdArray(store.getState())).toEqual([ 'ed2b4b7c-3388-53d9-b9f6-6a19d1ffde1f', 'plan-id-2', + 'plan-22', ]); expect(getPlansIdArray(store.getState(), InterventionType.IRS)).toEqual(['plan-id-2']); @@ -143,6 +146,12 @@ describe('reducers/plans', () => { const parentJurisdictionFilter = { parentJurisdictionId: '2977', }; + const titleFilter = { + title: 'John', + }; + const titleUpperFilter = { + title: 'JOHN', + }; expect(getPlansArrayByInterventionType()(store.getState(), {})).toEqual(values(allPlans)); expect(getPlansArrayByInterventionType()(store.getState(), fiFilter)).toEqual(values(fiPlans)); @@ -158,6 +167,8 @@ describe('reducers/plans', () => { expect( getPlansArrayByParentJurisdictionId()(store.getState(), parentJurisdictionFilter) ).toEqual(values(allPlans).filter(e => e.jurisdiction_path.includes('2977'))); + expect(getPlansArrayByTitle()(store.getState(), titleFilter)).toEqual([fixtures.plan22]); + expect(getPlansArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([fixtures.plan22]); expect( plansArraySelector(store.getState(), { ...fiFilter,