From 17a693aeb739ea96d56034c7209928ed0a7267f5 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 14 Apr 2020 16:42:34 +0300 Subject: [PATCH 01/30] Add search functionality for component ActiveFocusInvestigation Implement search by plan title by filtering case triggered plans and routine plans that match the search parameter --- .../pages/FocusInvestigation/active/index.tsx | 55 ++++++-- .../tests/__snapshots__/index.test.tsx.snap | 119 ++++++++++++++++++ .../active/tests/index.test.tsx | 70 +++++++++++ src/store/ducks/tests/fixtures.ts | 20 +++ 4 files changed, 256 insertions(+), 8 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index 4895b7b91e..a1e6eec9dc 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -11,7 +11,7 @@ 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, Form, FormGroup, Input, Row, Table } from 'reactstrap'; import { Store } from 'redux'; import { format } from 'util'; import DrillDownTableLinkedCell from '../../../../components/DrillDownTableLinkedCell'; @@ -40,7 +40,6 @@ import { PREVIOUS, REACTIVE, ROUTINE_TITLE, - SEARCH, START_DATE, STATUS_HEADER, } from '../../../../configs/lang'; @@ -96,6 +95,13 @@ export interface ActiveFIProps { plan: Plan | null; } +/** Interface defining component state */ +export interface ActiveFIState { + search?: string; + searchedCaseTriggeredPlans: Plan[]; + searchedRoutinePlans: Plan[]; +} + /** default props for ActiveFI component */ export const defaultActiveFIProps: ActiveFIProps = { caseTriggeredPlans: null, @@ -108,11 +114,18 @@ export const defaultActiveFIProps: ActiveFIProps = { /** Reporting for Active Focus Investigations */ class ActiveFocusInvestigation extends React.Component< ActiveFIProps & RouteComponentProps, - {} + ActiveFIState > { public static defaultProps: ActiveFIProps = defaultActiveFIProps; constructor(props: ActiveFIProps & RouteComponentProps) { super(props); + this.state = { + search: '', + searchedCaseTriggeredPlans: [], + searchedRoutinePlans: [], + }; + this.handleSubmit = this.handleSubmit.bind(this); + this.handleSearchChange = this.handleSearchChange.bind(this); } public componentDidMount() { @@ -124,9 +137,33 @@ class ActiveFocusInvestigation extends React.Component< .then((result: Plan[]) => fetchPlansActionCreator(result)) .catch(err => displayError(err)); } + public handleSubmit(event: React.FormEvent) { event.preventDefault(); } + + public handleSearchChange(event: React.ChangeEvent) { + this.setState({ search: event.target.value }, () => { + const { search } = this.state; + const { caseTriggeredPlans, routinePlans } = this.props; + + if (caseTriggeredPlans) { + this.setState({ + searchedCaseTriggeredPlans: caseTriggeredPlans.filter((plan: Plan) => + search ? plan.plan_title.toLowerCase().includes(search.toLowerCase()) : true + ), + }); + } + + if (routinePlans) { + this.setState({ + searchedRoutinePlans: routinePlans.filter((plan: Plan) => + search ? plan.plan_title.toLowerCase().includes(search.toLowerCase()) : true + ), + }); + } + }); + } public render() { const breadcrumbProps: BreadCrumbProps = { currentPage: { @@ -146,6 +183,7 @@ class ActiveFocusInvestigation extends React.Component< }; const { caseTriggeredPlans, routinePlans, plan } = this.props; + const { searchedCaseTriggeredPlans, searchedRoutinePlans, search } = this.state; // We need to initialize jurisdictionName to a falsy value let jurisdictionName = null; @@ -198,13 +236,14 @@ class ActiveFocusInvestigation extends React.Component< name="search" id="exampleEmail" placeholder="Search active focus investigations" + onChange={this.handleSearchChange} /> - - {[caseTriggeredPlans, routinePlans].forEach((plansArray: Plan[] | null, i) => { + {[ + search ? searchedCaseTriggeredPlans : caseTriggeredPlans, + search ? searchedRoutinePlans : routinePlans, + ].forEach((plansArray: Plan[] | null, i) => { const locationColumns: Column[] = getLocationColumns(locationHierarchy, true); if (plansArray && plansArray.length) { const jurisdictionValidPlans = removeNullJurisdictionPlans(plansArray); @@ -401,7 +440,7 @@ class ActiveFocusInvestigation extends React.Component< columns: emptyPlansColumns, }; routineReactivePlans.push( - + ); } })} 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..751777cb57 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,124 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans: case-triggered-table 1`] = ` +Array [ + Object { + "canton": "Chadiza", + "caseClassification": null, + "caseNotificationDate": "2019-06-18", + "district": null, + "focusArea": "NVI_439", + "id": "plan-id-3", + "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": "Random plan", + "plan_version": "1", + "province": null, + "reason": "Case Triggered", + "status": "A1", + "village": "Naviluli", + }, +] +`; + +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans: routine-table 1`] = `Array []`; + +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for routine plans: case-triggered-table 1`] = ` +Array [ + Object { + "canton": "Canton Tha Luang", + "caseClassification": null, + "caseNotificationDate": "2019-06-18", + "district": null, + "focusArea": "TLv1_02", + "id": "plan-id-2", + "jurisdiction_depth": 2, + "jurisdiction_id": "3378", + "jurisdiction_name": "TLv1_02", + "jurisdiction_name_path": Array [ + "Canton Tha Luang", + "Tha Luang Village", + ], + "jurisdiction_parent_id": "2977", + "jurisdiction_path": Array [ + "2989", + "2977", + ], + "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": "plan-id-2", + "plan_intervention_type": "IRS", + "plan_status": "active", + "plan_title": "A1-Tha Luang Village 1 Focus 01", + "plan_version": "1", + "province": null, + "reason": "Case Triggered", + "status": "A1", + "village": "Tha Luang Village", + }, +] +`; + +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for routine plans: routine-table 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..8aa106aa6c 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx +++ b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx @@ -230,4 +230,74 @@ describe('containers/pages/ActiveFocusInvestigation', () => { expect(supersetMock).toHaveBeenCalledWith(0, supersetParams); wrapper.unmount(); }); + + it('handles search correctly for case triggered plans', () => { + const mock: any = jest.fn(); + const props = { + caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], + fetchPlansActionCreator: jest.fn(), + history, + location: mock, + match: mock, + routinePlans: [fixtures.plan1], + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'random' } }); + wrapper.update(); + expect( + wrapper + .find('ReactTable') + .at(0) + .prop('data') + ).toMatchSnapshot('case-triggered-table'); + expect( + wrapper + .find('ReactTable') + .at(1) + .prop('data') + ).toMatchSnapshot('routine-table'); + }); + + it('handles search correctly for routine plans', () => { + const mock: any = jest.fn(); + const props = { + caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], + fetchPlansActionCreator: jest.fn(), + history, + location: mock, + match: mock, + routinePlans: [fixtures.plan1], + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'Luang' } }); + wrapper.update(); + expect( + wrapper + .find('ReactTable') + .at(0) + .prop('data') + ).toMatchSnapshot('case-triggered-table'); + expect( + wrapper + .find('ReactTable') + .at(1) + .prop('data') + ).toMatchSnapshot('routine-table'); + }); }); diff --git a/src/store/ducks/tests/fixtures.ts b/src/store/ducks/tests/fixtures.ts index b957df9a97..695e57e1e9 100644 --- a/src/store/ducks/tests/fixtures.ts +++ b/src/store/ducks/tests/fixtures.ts @@ -45,6 +45,26 @@ export const plan2: Plan = { plan_version: '1', }; +export const plan23: Plan = { + id: 'plan-id-3', + 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: 'Random plan', + plan_version: '1', +}; + export const draftPlan = { id: 'draftPlan-id-2', jurisdiction_depth: 41, From a5e735743a3674119f50fdbc03ac7f76344b4340 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 14 Apr 2020 19:19:58 +0300 Subject: [PATCH 02/30] Add search for component IRSPlansList --- .../pages/FocusInvestigation/active/index.tsx | 5 +- .../tests/__snapshots__/index.test.tsx.snap | 41 +++++++++++ .../active/tests/index.test.tsx | 70 +++++++++++++++++++ src/containers/pages/IRS/plans/index.tsx | 42 ++++++++--- .../pages/IRS/plans/tests/index.test.tsx | 69 ++++++++++++++++++ 5 files changed, 216 insertions(+), 11 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index a1e6eec9dc..c679fa81ed 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -229,13 +229,12 @@ class ActiveFocusInvestigation extends React.Component<

{pageTitle}


-
+ 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 751777cb57..1123906ba5 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,46 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`containers/pages/ActiveFocusInvestigation handles case insensitive searches correctly: case-triggered-table 1`] = ` +Array [ + Object { + "canton": "Chadiza", + "caseClassification": null, + "caseNotificationDate": "2019-06-18", + "district": null, + "focusArea": "NVI_439", + "id": "plan-id-3", + "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": "Random plan", + "plan_version": "1", + "province": null, + "reason": "Case Triggered", + "status": "A1", + "village": "Naviluli", + }, +] +`; + +exports[`containers/pages/ActiveFocusInvestigation handles case insensitive searches correctly: routine-table 1`] = `Array []`; + exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans: case-triggered-table 1`] = ` Array [ Object { diff --git a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx index 8aa106aa6c..1df1a71c42 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx +++ b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx @@ -300,4 +300,74 @@ describe('containers/pages/ActiveFocusInvestigation', () => { .prop('data') ).toMatchSnapshot('routine-table'); }); + + it('handles case insensitive searches correctly', () => { + const mock: any = jest.fn(); + const props = { + caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], + fetchPlansActionCreator: jest.fn(), + history, + location: mock, + match: mock, + routinePlans: [fixtures.plan1], + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'RANDOM' } }); + wrapper.update(); + expect( + wrapper + .find('ReactTable') + .at(0) + .prop('data') + ).toMatchSnapshot('case-triggered-table'); + expect( + wrapper + .find('ReactTable') + .at(1) + .prop('data') + ).toMatchSnapshot('routine-table'); + }); + + it('renders empty tables if search query does not match any case trigger or routine plans', () => { + const mock: any = jest.fn(); + const props = { + caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], + fetchPlansActionCreator: jest.fn(), + history, + location: mock, + match: mock, + routinePlans: [fixtures.plan1], + supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'kajkajakjTTyaa' } }); + wrapper.update(); + 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 8ee67b9986..aef7d3e581 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; -import { Col, Row } from 'reactstrap'; +import { Col, Form, FormGroup, Input, Row } from 'reactstrap'; import { Store } from 'redux'; import HeaderBreadcrumb from '../../../../components/page/HeaderBreadcrumb/HeaderBreadcrumb'; import Loading from '../../../../components/page/Loading'; @@ -42,7 +42,10 @@ interface PlanListProps { /** Simple component that loads the new plan form and allows you to create a new plan */ const IRSPlansList = (props: PlanListProps) => { const { fetchPlans, plans, service } = props; - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); + const defaultSearchedPlans: IRSPlan[] = []; + const [searchedPlans, setSearchedPlans] = useState(defaultSearchedPlans); + const [searchQuery, setSearchQuery] = useState(''); const pageTitle: string = IRS_PLANS; @@ -73,16 +76,26 @@ const IRSPlansList = (props: PlanListProps) => { } } + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + useEffect(() => { loadData().catch(error => displayError(error)); }, []); - if (loading === true) { - return ; - } + useEffect(() => { + if (plans) { + setSearchedPlans( + plans.filter((plan: IRSPlan) => + searchQuery ? plan.plan_title.toLowerCase().includes(searchQuery.toLowerCase()) : true + ) + ); + } + }, [searchQuery]); - const listViewProps = { - data: plans.map(planObj => { + const listViewData = (planList: IRSPlan[]) => + planList.map(planObj => { return [ {planObj.plan_title} @@ -92,7 +105,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(searchQuery ? searchedPlans : plans), headerItems: [TITLE, DATE_CREATED, START_DATE, END_DATE, STATUS_HEADER], tableClass: 'table table-bordered plans-list', }; @@ -108,6 +128,12 @@ const IRSPlansList = (props: PlanListProps) => {

{pageTitle}

+
+ + + + + diff --git a/src/containers/pages/IRS/plans/tests/index.test.tsx b/src/containers/pages/IRS/plans/tests/index.test.tsx index 025b1badb0..6f4e20a98a 100644 --- a/src/containers/pages/IRS/plans/tests/index.test.tsx +++ b/src/containers/pages/IRS/plans/tests/index.test.tsx @@ -44,4 +44,73 @@ describe('components/IRS Reports/IRSPlansList', () => { expect(toJson(wrapper.find('tbody tr td'))).toMatchSnapshot('table rows'); wrapper.unmount(); }); + + it('handles search correctly', () => { + const props = { + fetchPlans: jest.fn(), + plans: fixtures.plans as IRSPlan[], + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'Berg' } }); + wrapper.mount(); + expect( + wrapper + .find('tbody tr td') + .find('Link') + .at(0) + .text() + ).toEqual('Berg Namibia 2019'); + }); + + it('handles a case insensitive search', () => { + const props = { + fetchPlans: jest.fn(), + plans: fixtures.plans as IRSPlan[], + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'BERG' } }); + 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', () => { + const props = { + fetchPlans: jest.fn(), + plans: fixtures.plans as IRSPlan[], + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'OOOOOOOPPOAPOPAO' } }); + wrapper.mount(); + expect(toJson(wrapper.find('tbody tr'))).toEqual(null); + }); }); From a8f343a98ec0ba37b212b079f93aece4873901d2 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 14 Apr 2020 19:51:52 +0300 Subject: [PATCH 03/30] Refactor search forms into a single reusable component --- src/components/forms/Search/index.tsx | 19 +++++++++ .../tests/__snapshots__/index.test.tsx.snap | 40 +++++++++++++++++++ .../forms/Search/tests/index.test.tsx | 15 +++++++ .../pages/FocusInvestigation/active/index.tsx | 14 ++----- src/containers/pages/IRS/plans/index.tsx | 9 ++--- 5 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 src/components/forms/Search/index.tsx create mode 100644 src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap create mode 100644 src/components/forms/Search/tests/index.test.tsx diff --git a/src/components/forms/Search/index.tsx b/src/components/forms/Search/index.tsx new file mode 100644 index 0000000000..b25f9fcea7 --- /dev/null +++ b/src/components/forms/Search/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Form, FormGroup, Input } from 'reactstrap'; + +export type SearchChange = (event: React.ChangeEvent) => void; + +/** Interface for SearchForm props */ +export interface SearchFormProps { + handleSearchChange: SearchChange; +} + +export const SearchForm = (props: SearchFormProps) => { + return ( +
+ + + +
+ ); +}; 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..bbcec553c3 --- /dev/null +++ b/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap @@ -0,0 +1,40 @@ +// 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..f7a73d9220 --- /dev/null +++ b/src/components/forms/Search/tests/index.test.tsx @@ -0,0 +1,15 @@ +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import React from 'react'; +import { SearchForm } from '../../Search'; + +describe('src/components/SearchForm', () => { + it('renders correctly', () => { + const props = { + handleSearchChange: jest.fn(), + }; + const wrapper = mount(); + expect(toJson(wrapper)).toMatchSnapshot(); + wrapper.unmount(); + }); +}); diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index c679fa81ed..23c3edd766 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 { 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, { @@ -229,16 +230,7 @@ class ActiveFocusInvestigation extends React.Component<

{pageTitle}


-
- - - -
+ {[ search ? searchedCaseTriggeredPlans : caseTriggeredPlans, search ? searchedRoutinePlans : routinePlans, diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index aef7d3e581..1e37c808e9 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -4,8 +4,9 @@ import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; -import { Col, Form, FormGroup, Input, Row } from 'reactstrap'; +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'; @@ -129,11 +130,7 @@ const IRSPlansList = (props: PlanListProps) => {

-
- - - -
+ From 93e6df77e0936e1cc3622ffdb13b80eefb47538f Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Wed, 15 Apr 2020 10:17:35 +0300 Subject: [PATCH 04/30] Add search for manage plans list --- src/containers/pages/IRS/plans/index.tsx | 3 +- .../PlanDefinitionList/index.tsx | 34 +++++++-- .../tests/__snapshots__/index.test.tsx.snap | 37 ++++++++++ .../PlanDefinitionList/tests/index.test.tsx | 69 +++++++++++++++++++ 4 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index 1e37c808e9..a5aa2b0f01 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -44,8 +44,7 @@ interface PlanListProps { const IRSPlansList = (props: PlanListProps) => { const { fetchPlans, plans, service } = props; const [loading, setLoading] = useState(false); - const defaultSearchedPlans: IRSPlan[] = []; - const [searchedPlans, setSearchedPlans] = useState(defaultSearchedPlans); + const [searchedPlans, setSearchedPlans] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const pageTitle: string = IRS_PLANS; diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx index 31587ac9a5..be01c9f49c 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; 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'; @@ -42,6 +43,8 @@ interface PlanListProps { const PlanDefinitionList = (props: PlanListProps) => { const { fetchPlans, plans, service } = props; const [loading, setLoading] = useState(true); + const [searchedPlans, setSearchedPlans] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); const apiService = new service(OPENSRP_PLANS); @@ -77,12 +80,22 @@ const PlanDefinitionList = (props: PlanListProps) => { loadData().catch(err => displayError(err)); }, []); - if (loading === true) { - return ; - } + useEffect(() => { + if (plans) { + setSearchedPlans( + plans.filter((plan: PlanDefinition) => + searchQuery ? plan.name.toLowerCase().includes(searchQuery.toLowerCase()) : true + ) + ); + } + }, [searchQuery]); - const listViewProps = { - data: plans.map(planObj => { + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + + const listViewData = (data: PlanDefinition[]) => + data.map(planObj => { const typeUseContext = planObj.useContext.filter(e => e.code === 'interventionType'); return [ @@ -93,7 +106,14 @@ const PlanDefinitionList = (props: PlanListProps) => { planStatusDisplay[planObj.status] || planObj.status, planObj.date, ]; - }), + }); + + if (loading === true) { + return ; + } + + const listViewProps = { + data: listViewData(searchQuery ? searchedPlans : plans), headerItems: [TITLE, INTERVENTION_TYPE_LABEL, STATUS_HEADER, LAST_MODIFIED], tableClass: 'table table-bordered plans-list', }; @@ -115,6 +135,8 @@ const PlanDefinitionList = (props: PlanListProps) => { /> +
+ 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..ef101907c2 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 @@ -1036,6 +1036,43 @@ exports[`components/InterventionPlan/PlanDefinitionList renders plan definition +
+ +
+ + +
+ + + +
+
+
+ +
diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index c8ab3963a1..b7dbc27ffe 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -40,4 +40,73 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { expect(toJson(wrapper)).toMatchSnapshot(); wrapper.unmount(); }); + + it('handles search correctly', () => { + const props = { + fetchPlans: jest.fn(), + plans: fixtures.plans, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'Mosh' } }); + wrapper.mount(); + expect( + wrapper + .find('tbody tr td') + .find('Link') + .at(0) + .text() + ).toEqual('A Test By Mosh'); + }); + + it('handles a case insensitive search', () => { + const props = { + fetchPlans: jest.fn(), + plans: fixtures.plans, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'MOsh' } }); + 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', () => { + const props = { + fetchPlans: jest.fn(), + plans: fixtures.plans, + service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), + }; + const wrapper = mount( + + + + ); + wrapper + .find('Input') + .at(0) + .simulate('change', { target: { value: 'OOOOOOOPPOAPOPAO' } }); + wrapper.mount(); + expect(toJson(wrapper.find('tbody tr'))).toEqual(null); + }); }); From 2d7986fc000a0a9806fc33aa37f2b8c70b6b3426 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Wed, 15 Apr 2020 12:06:35 +0300 Subject: [PATCH 05/30] Debounce search Delay search to make sure the filter is not applied to frequently --- .../pages/FocusInvestigation/active/index.tsx | 16 ++++++---- .../active/tests/index.test.tsx | 16 +++++++--- src/containers/pages/IRS/plans/index.tsx | 11 +++++-- .../pages/IRS/plans/tests/index.test.tsx | 12 +++++-- .../PlanDefinitionList/index.tsx | 12 ++++--- .../PlanDefinitionList/tests/index.test.tsx | 12 +++++-- src/helpers/hooks.tsx | 32 +++++++++++++++++++ 7 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index 23c3edd766..00338bf265 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -4,6 +4,7 @@ import DrillDownTable from '@onaio/drill-down-table'; import { FlexObject } from '@onaio/drill-down-table/dist/types/helpers/utils'; import reducerRegistry from '@onaio/redux-reducer-registry'; import superset from '@onaio/superset-connector'; +import _ from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; @@ -118,6 +119,8 @@ class ActiveFocusInvestigation extends React.Component< ActiveFIState > { public static defaultProps: ActiveFIProps = defaultActiveFIProps; + + public debouncedSearchCallback = _.debounce(this.debouncedSearch, 1000); constructor(props: ActiveFIProps & RouteComponentProps) { super(props); this.state = { @@ -125,8 +128,8 @@ class ActiveFocusInvestigation extends React.Component< searchedCaseTriggeredPlans: [], searchedRoutinePlans: [], }; - this.handleSubmit = this.handleSubmit.bind(this); this.handleSearchChange = this.handleSearchChange.bind(this); + this.debouncedSearchCallback = _.debounce(this.debouncedSearch, 1000); } public componentDidMount() { @@ -139,11 +142,7 @@ class ActiveFocusInvestigation extends React.Component< .catch(err => displayError(err)); } - public handleSubmit(event: React.FormEvent) { - event.preventDefault(); - } - - public handleSearchChange(event: React.ChangeEvent) { + public debouncedSearch(event: React.ChangeEvent) { this.setState({ search: event.target.value }, () => { const { search } = this.state; const { caseTriggeredPlans, routinePlans } = this.props; @@ -165,6 +164,11 @@ class ActiveFocusInvestigation extends React.Component< } }); } + + public handleSearchChange(event: React.ChangeEvent) { + event.persist(); // This will ensure that the event is not pooled for more details https://reactjs.org/docs/events.html + this.debouncedSearchCallback(event); + } public render() { const breadcrumbProps: BreadCrumbProps = { currentPage: { diff --git a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx index 1df1a71c42..8e3ec272a7 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx +++ b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx @@ -231,7 +231,7 @@ describe('containers/pages/ActiveFocusInvestigation', () => { wrapper.unmount(); }); - it('handles search correctly for case triggered plans', () => { + it('handles search correctly for case triggered plans', async () => { const mock: any = jest.fn(); const props = { caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], @@ -251,6 +251,8 @@ describe('containers/pages/ActiveFocusInvestigation', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'random' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1000)); wrapper.update(); expect( wrapper @@ -266,7 +268,7 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ).toMatchSnapshot('routine-table'); }); - it('handles search correctly for routine plans', () => { + it('handles search correctly for routine plans', async () => { const mock: any = jest.fn(); const props = { caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], @@ -286,6 +288,8 @@ describe('containers/pages/ActiveFocusInvestigation', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'Luang' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1000)); wrapper.update(); expect( wrapper @@ -301,7 +305,7 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ).toMatchSnapshot('routine-table'); }); - it('handles case insensitive searches correctly', () => { + it('handles case insensitive searches correctly', async () => { const mock: any = jest.fn(); const props = { caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], @@ -321,6 +325,8 @@ describe('containers/pages/ActiveFocusInvestigation', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'RANDOM' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1000)); wrapper.update(); expect( wrapper @@ -336,7 +342,7 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ).toMatchSnapshot('routine-table'); }); - it('renders empty tables if search query does not match any case trigger or routine plans', () => { + it('renders empty tables if search query does not match any case trigger or routine plans', async () => { const mock: any = jest.fn(); const props = { caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], @@ -356,6 +362,8 @@ describe('containers/pages/ActiveFocusInvestigation', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'kajkajakjTTyaa' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1000)); wrapper.update(); expect( wrapper diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index a5aa2b0f01..c7c43728ed 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -22,6 +22,7 @@ import { import { planStatusDisplay } from '../../../../configs/settings'; import { HOME_URL, REPORT_IRS_PLAN_URL } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; +import { useDebounce } from '../../../../helpers/hooks'; import supersetFetch from '../../../../services/superset'; import IRSPlansReducer, { fetchIRSPlans, @@ -46,8 +47,10 @@ const IRSPlansList = (props: PlanListProps) => { const [loading, setLoading] = useState(false); const [searchedPlans, setSearchedPlans] = useState([]); const [searchQuery, setSearchQuery] = useState(''); - const pageTitle: string = IRS_PLANS; + // Return the latest search query if it's been more than 1s since + // it was last called + const debouncedSearchQuery = useDebounce(searchQuery); const breadcrumbProps = { currentPage: { @@ -88,11 +91,13 @@ const IRSPlansList = (props: PlanListProps) => { if (plans) { setSearchedPlans( plans.filter((plan: IRSPlan) => - searchQuery ? plan.plan_title.toLowerCase().includes(searchQuery.toLowerCase()) : true + debouncedSearchQuery + ? plan.plan_title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) + : true ) ); } - }, [searchQuery]); + }, [debouncedSearchQuery]); const listViewData = (planList: IRSPlan[]) => planList.map(planObj => { diff --git a/src/containers/pages/IRS/plans/tests/index.test.tsx b/src/containers/pages/IRS/plans/tests/index.test.tsx index 6f4e20a98a..e8e48d04c2 100644 --- a/src/containers/pages/IRS/plans/tests/index.test.tsx +++ b/src/containers/pages/IRS/plans/tests/index.test.tsx @@ -45,7 +45,7 @@ describe('components/IRS Reports/IRSPlansList', () => { wrapper.unmount(); }); - it('handles search correctly', () => { + it('handles search correctly', async () => { const props = { fetchPlans: jest.fn(), plans: fixtures.plans as IRSPlan[], @@ -60,6 +60,8 @@ describe('components/IRS Reports/IRSPlansList', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'Berg' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect( wrapper @@ -70,7 +72,7 @@ describe('components/IRS Reports/IRSPlansList', () => { ).toEqual('Berg Namibia 2019'); }); - it('handles a case insensitive search', () => { + it('handles a case insensitive search', async () => { const props = { fetchPlans: jest.fn(), plans: fixtures.plans as IRSPlan[], @@ -85,6 +87,8 @@ describe('components/IRS Reports/IRSPlansList', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'BERG' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect( wrapper @@ -95,7 +99,7 @@ describe('components/IRS Reports/IRSPlansList', () => { ).toEqual('Berg Namibia 2019'); }); - it('renders empty table if no search matches', () => { + it('renders empty table if no search matches', async () => { const props = { fetchPlans: jest.fn(), plans: fixtures.plans as IRSPlan[], @@ -110,6 +114,8 @@ describe('components/IRS Reports/IRSPlansList', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'OOOOOOOPPOAPOPAO' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1500)); 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 be01c9f49c..5e62d0bf08 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -22,6 +22,7 @@ import { import { PlanDefinition, planStatusDisplay } from '../../../../configs/settings'; import { HOME_URL, OPENSRP_PLANS, PLAN_LIST_URL, PLAN_UPDATE_URL } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; +import { useDebounce } from '../../../../helpers/hooks'; import { OpenSRPService } from '../../../../services/opensrp'; import planDefinitionReducer, { fetchPlanDefinitions, @@ -45,10 +46,11 @@ const PlanDefinitionList = (props: PlanListProps) => { const [loading, setLoading] = useState(true); const [searchedPlans, setSearchedPlans] = useState([]); const [searchQuery, setSearchQuery] = useState(''); - const apiService = new service(OPENSRP_PLANS); - const pageTitle: string = PLANS; + // Return the latest search query if it's been more than 1s since + // it was last called + const debouncedSearchQuery = useDebounce(searchQuery); const breadcrumbProps = { currentPage: { @@ -84,11 +86,13 @@ const PlanDefinitionList = (props: PlanListProps) => { if (plans) { setSearchedPlans( plans.filter((plan: PlanDefinition) => - searchQuery ? plan.name.toLowerCase().includes(searchQuery.toLowerCase()) : true + debouncedSearchQuery + ? plan.name.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) + : true ) ); } - }, [searchQuery]); + }, [debouncedSearchQuery]); const handleSearchChange = (event: React.ChangeEvent) => { setSearchQuery(event.target.value); diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index b7dbc27ffe..52af2461fb 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -41,7 +41,7 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { wrapper.unmount(); }); - it('handles search correctly', () => { + it('handles search correctly', async () => { const props = { fetchPlans: jest.fn(), plans: fixtures.plans, @@ -56,6 +56,8 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'Mosh' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect( wrapper @@ -66,7 +68,7 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ).toEqual('A Test By Mosh'); }); - it('handles a case insensitive search', () => { + it('handles a case insensitive search', async () => { const props = { fetchPlans: jest.fn(), plans: fixtures.plans, @@ -81,6 +83,8 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'MOsh' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect( wrapper @@ -91,7 +95,7 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ).toEqual('A Test By Mosh'); }); - it('renders empty table if no search matches', () => { + it('renders empty table if no search matches', async () => { const props = { fetchPlans: jest.fn(), plans: fixtures.plans, @@ -106,6 +110,8 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { .find('Input') .at(0) .simulate('change', { target: { value: 'OOOOOOOPPOAPOPAO' } }); + // Wait for debounce + await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect(toJson(wrapper.find('tbody tr'))).toEqual(null); }); diff --git a/src/helpers/hooks.tsx b/src/helpers/hooks.tsx index 3e4c1b433a..9f50dd4c01 100644 --- a/src/helpers/hooks.tsx +++ b/src/helpers/hooks.tsx @@ -17,3 +17,35 @@ export function useConfirmOnBrowserUnload(hasUnsavedChanges: boolean = false) { return () => window.removeEventListener('beforeunload', callback); }); } + +/** + * Debounce calls to make sure they do not execute too frequently such as when + * a user is typing and a call needs to be made to the API + * @param value Value to be debounced + * @param delay Time in ms to wait for since the last call + */ +export function useDebounce(value: string, delay: number = 1000) { + // State and setters for debounced value + const [debouncedValue, setDebouncedValue] = React.useState(value); + + React.useEffect(() => { + // Set debouncedValue to value (passed in) after the specified delay + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + // Return a cleanup function that will be called every time ... + // ... useEffect is re-called. useEffect will only be re-called ... + // ... if value changes (see the inputs array below). + // This is how we prevent debouncedValue from changing if value is ... + // ... changed within the delay period. Timeout gets cleared and restarted. + // To put it in context, if the user is typing within our app's ... + // ... search box, we don't want the debouncedValue to update until ... + // ... they've stopped typing for more than 1000ms. + return () => { + clearTimeout(handler); + }; + }, [value]); + + return debouncedValue; +} From 5e0329bd77c3328d09142dbcd5c9a9840b935947 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Thu, 16 Apr 2020 15:28:23 +0300 Subject: [PATCH 06/30] Add getPlansArrayByTitle selector --- src/components/page/WithGATracker/index.tsx | 137 ------------------ .../tests/__snapshots__/index.test.tsx.snap | 4 +- src/store/ducks/plans.ts | 30 +++- src/store/ducks/tests/fixtures.ts | 2 +- src/store/ducks/tests/plans.test.ts | 15 +- 5 files changed, 42 insertions(+), 146 deletions(-) delete mode 100644 src/components/page/WithGATracker/index.tsx diff --git a/src/components/page/WithGATracker/index.tsx b/src/components/page/WithGATracker/index.tsx deleted file mode 100644 index ea8a603bff..0000000000 --- a/src/components/page/WithGATracker/index.tsx +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint-disable react/prop-types */ -import { getUser, User } from '@onaio/session-reducer'; -import React, { Component } from 'react'; -import GoogleAnalytics from 'react-ga'; -import { RouteComponentProps } from 'react-router'; -import * as env from '../../../configs/env'; -import { ConnectedFlexComponent, FlexComponent } from '../../../configs/types'; -import { GA_ENV_TEST } from '../../../constants'; -import { RouteParams } from '../../../helpers/utils'; -import store from '../../../store'; - -type Props = RouteComponentProps; -let username = (getUser(store.getState()) || {}).username || ''; - -/** - * Interface defining the options param passed into tracking methods - */ -export interface TrackingOptions { - GA_CODE: string; - GA_ENV?: string; -} - -/** - * Default tracking options using global envs - */ -export const defaultTrackingOptions: TrackingOptions = { - GA_CODE: env.GA_CODE || '', - GA_ENV: env.GA_ENV || GA_ENV_TEST, -}; - -/** - * helper function to set the Google Analytics dimension for username - * @param {User} user user object returned from session store - */ -export const setGAusername = (user: User): void => { - username = user.username || ''; - GoogleAnalytics.set({ username }); -}; - -/** - * helper function to get the current value for the username GA dimension - * @returns {string} the username or '' - */ -export const getGAusername = (): string => username; - -/** - * helper function to execute the page view Google Analytics tracking - * @param {string} page the url string of the page view being tracked - * @param {Dictionary} options tracking options for the page view - */ -export const trackPage = ( - page: string, - options: TrackingOptions = defaultTrackingOptions -): void => { - const { GA_CODE } = options; - if (GA_CODE && GA_CODE.length) { - GoogleAnalytics.set({ - page, - ...options, - }); - GoogleAnalytics.pageview(page); - } -}; - -/** - * helper function to initialize GoogleAnalytics - * @param {TrackingOptions} options tracking options for the page view - * @returns {TrackingOptions} - */ -export const initGoogleAnalytics = ( - options: TrackingOptions = defaultTrackingOptions -): TrackingOptions => { - const { GA_CODE, GA_ENV } = options; - if (GA_CODE && GA_CODE.length) { - GoogleAnalytics.initialize(GA_CODE, { - testMode: GA_ENV === GA_ENV_TEST, - }); - GoogleAnalytics.set({ - env: GA_ENV || GA_ENV_TEST, - username, - }); - } - return { ...options }; -}; - -/** - * Higher Order Component (HOC) which handles Google Analytics page view tracking - * @param {FlexComponent | ConnectedFlexComponent} WrappedComponent the component to be wrapped by the HOC component - * @param {TrackingOptions} options tracking options for the page view - * @returns HOC rendering the WrappedComponent - */ -const WithGATracker = ( - WrappedComponent: FlexComponent | ConnectedFlexComponent, - options: TrackingOptions = defaultTrackingOptions -) => { - const { GA_CODE } = options; - if (!GA_CODE || !GA_CODE.length) { - return WrappedComponent; - } - - const WithGATrackerHOC = class extends Component { - public componentDidMount() { - // update the username dimension - const user: User = getUser(store.getState()) || {}; - if (user.username && getGAusername() !== user.username) { - setGAusername(user); - } - - // track the page view - if (GA_CODE.length) { - const page = `${this.props.location.pathname}${this.props.location.search}`; - trackPage(page, options); - } - } - - public componentDidUpdate(prevProps: Props) { - if (GA_CODE.length) { - const { location } = this.props; - const currentPage = prevProps.location.pathname + location.search; - const nextPage = location.pathname + location.search; - - // track the page view here only if component didn't un/remount and the URL has updated - if (currentPage !== nextPage) { - trackPage(nextPage, options); - } - } - } - - public render() { - return ; - } - }; - - return WithGATrackerHOC; -}; - -export default WithGATracker; 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 1123906ba5..cf2a71318a 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 @@ -8,7 +8,7 @@ Array [ "caseNotificationDate": "2019-06-18", "district": null, "focusArea": "NVI_439", - "id": "plan-id-3", + "id": "plan-id-23", "jurisdiction_depth": 2, "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", "jurisdiction_name": "NVI_439", @@ -49,7 +49,7 @@ Array [ "caseNotificationDate": "2019-06-18", "district": null, "focusArea": "NVI_439", - "id": "plan-id-3", + "id": "plan-id-23", "jurisdiction_depth": 2, "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", "jurisdiction_name": "NVI_439", diff --git a/src/store/ducks/plans.ts b/src/store/ducks/plans.ts index f01cf343a7..c7948ff6d5 100644 --- a/src/store/ducks/plans.ts +++ b/src/store/ducks/plans.ts @@ -1,12 +1,11 @@ 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'; import { AnyAction, Store } from 'redux'; import { createSelector } from 'reselect'; import SeamlessImmutable from 'seamless-immutable'; import { FIReasonType, FIStatusType } from '../../components/forms/PlanForm/types'; -import { descendingOrderSort, removeNullJurisdictionPlans } from '../../helpers/utils'; +import { descendingOrderSort, FlexObject, removeNullJurisdictionPlans } from '../../helpers/utils'; /** the reducer name */ export const reducerName = 'plans'; @@ -105,13 +104,13 @@ export enum PlanEventType { export interface PlanEventPayload { baseEntityId: string; dateCreated: string; - details: Dictionary; + details: FlexObject; duration: number; entityType: InterventionType; eventDate: string; eventType: PlanEventType; formSubmissionId: string; - identifiers: Dictionary; + identifiers: FlexObject; obs: Array<{ fieldType: 'concept'; fieldDataType: string; @@ -389,6 +388,7 @@ 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 @@ -433,6 +433,13 @@ export const getStatusList = (_: Registry, props: PlanFilters) => props.statusLi */ export const getReason = (_: Registry, props: PlanFilters) => props.reason; +/** getTitle + * Gets title from PlanFilters + * @param state - the redux store + * @param props - the plan filters object + */ +export const getTitle = (_: Registry, props: PlanFilters) => props.title; + /** getPlansArrayByInterventionType * Gets an array of Plan objects filtered by interventionType * @param {Registry} state - the redux store @@ -500,6 +507,19 @@ export const getPlansArrayByParentJurisdictionId = (planKey?: string) => : true) ) ); + +/** getPlansArrayByTitle + * Gets an array of Plan objects filtered by plan title + * @param {Registry} 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 +528,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. * @@ -528,6 +549,7 @@ export const makePlansArraySelector = (planKey?: string, sortField?: string) => getPlansArrayByStatus(planKey), getPlansArrayByReason(planKey), getPlansArrayByParentJurisdictionId(planKey), + getPlansArrayByTitle(planKey), ], (plans, plans2, plans3, plans4, plans5) => sortField diff --git a/src/store/ducks/tests/fixtures.ts b/src/store/ducks/tests/fixtures.ts index d1e05c23bb..95d6aff121 100644 --- a/src/store/ducks/tests/fixtures.ts +++ b/src/store/ducks/tests/fixtures.ts @@ -46,7 +46,7 @@ export const plan2: Plan = { }; export const plan23: Plan = { - id: 'plan-id-3', + id: 'plan-id-23', jurisdiction_depth: 2, jurisdiction_id: '450fc15b-5bd2-468a-927a-49cb10d3bcac', jurisdiction_name: 'NVI_439', diff --git a/src/store/ducks/tests/plans.test.ts b/src/store/ducks/tests/plans.test.ts index d03a8b376a..f7bfecba5a 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.plan23])); const plansArraySelector = makePlansArraySelector(); - const allPlans = keyBy(fixtures.plans, (plan: Plan) => plan.id); + const allPlans = keyBy([...fixtures.plans, fixtures.plan23], (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-id-23', ]); expect(getPlansIdArray(store.getState(), InterventionType.IRS)).toEqual(['plan-id-2']); @@ -143,6 +146,12 @@ describe('reducers/plans', () => { const parentJurisdictionFilter = { parentJurisdictionId: '2977', }; + const titleFilter = { + title: 'Random', + }; + const titleUpperFilter = { + title: 'RANDOM', + }; 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.plan23]); + expect(getPlansArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([fixtures.plan23]); expect( plansArraySelector(store.getState(), { ...fiFilter, From efddc76788c852aeb9a6d88b9307e6d04677946b Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Fri, 17 Apr 2020 20:10:45 +0300 Subject: [PATCH 07/30] Make use of reselect for Focus investigation search --- .../pages/FocusInvestigation/active/index.tsx | 41 ++++---- .../tests/__snapshots__/index.test.tsx.snap | 63 ++---------- .../active/tests/index.test.tsx | 95 +++++++++---------- src/store/ducks/plans.ts | 13 +-- src/store/ducks/tests/fixtures.ts | 60 ++++++++++++ 5 files changed, 144 insertions(+), 128 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index 78ff5d62fa..8985d43214 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -68,11 +68,13 @@ import { } from '../../../../helpers/utils'; import { extractPlan, getLocationColumns } from '../../../../helpers/utils'; import supersetFetch from '../../../../services/superset'; +import store from '../../../../store'; import plansReducer, { fetchPlans, getPlanById, getPlansArray, InterventionType, + makePlansArraySelector, Plan, PlanStatus, reducerName as plansReducerName, @@ -95,6 +97,7 @@ export interface ActiveFIProps { routinePlans: Plan[] | null; supersetService: typeof supersetFetch; plan: Plan | null; + jurisdictionParentId: string; } /** Interface defining component state */ @@ -108,6 +111,7 @@ export interface ActiveFIState { export const defaultActiveFIProps: ActiveFIProps = { caseTriggeredPlans: null, fetchPlansActionCreator: fetchPlans, + jurisdictionParentId: '', plan: null, routinePlans: null, supersetService: supersetFetch, @@ -144,24 +148,25 @@ class ActiveFocusInvestigation extends React.Component< public debouncedSearch(event: React.ChangeEvent) { this.setState({ search: event.target.value }, () => { - const { search } = this.state; - const { caseTriggeredPlans, routinePlans } = this.props; - - if (caseTriggeredPlans) { - this.setState({ - searchedCaseTriggeredPlans: caseTriggeredPlans.filter((plan: Plan) => - search ? plan.plan_title.toLowerCase().includes(search.toLowerCase()) : true - ), - }); - } + this.setState({ + searchedCaseTriggeredPlans: makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: this.props.jurisdictionParentId, + reason: CASE_TRIGGERED, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: event.target.value, + }), + }); - if (routinePlans) { - this.setState({ - searchedRoutinePlans: routinePlans.filter((plan: Plan) => - search ? plan.plan_title.toLowerCase().includes(search.toLowerCase()) : true - ), - }); - } + this.setState({ + searchedRoutinePlans: makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: this.props.jurisdictionParentId, + reason: ROUTINE, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: event.target.value, + }), + }); }); } @@ -455,6 +460,7 @@ export { ActiveFocusInvestigation }; /** interface to describe props from mapStateToProps */ interface DispatchedStateProps { + jurisdictionParentId: string; plan: Plan | null; caseTriggeredPlans: Plan[] | null; routinePlans: Plan[] | null; @@ -487,6 +493,7 @@ const mapStateToProps = (state: Partial, ownProps: any): DispatchedStateP ); return { caseTriggeredPlans, + jurisdictionParentId, plan, routinePlans, }; 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 cf2a71318a..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,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`containers/pages/ActiveFocusInvestigation handles case insensitive searches correctly: case-triggered-table 1`] = ` +exports[`containers/pages/ActiveFocusInvestigation handles case insensitive searches correctly 1`] = ` Array [ Object { "canton": "Chadiza", "caseClassification": null, - "caseNotificationDate": "2019-06-18", + "caseNotificationDate": null, "district": null, "focusArea": "NVI_439", - "id": "plan-id-23", + "id": "ed2b4b7c-3388-53d9-b9f6-6a19d1ffde1f", "jurisdiction_depth": 2, "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", "jurisdiction_name": "NVI_439", @@ -24,24 +24,22 @@ Array [ "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_reason": "Routine", "plan_fi_status": "A1", "plan_id": "10f9e9fa-ce34-4b27-a961-72fab5206ab6", "plan_intervention_type": "FI", "plan_status": "active", - "plan_title": "Random plan", + "plan_title": "A1-Tha Luang Village 1 Focus 01", "plan_version": "1", "province": null, - "reason": "Case Triggered", + "reason": "Routine", "status": "A1", "village": "Naviluli", }, ] `; -exports[`containers/pages/ActiveFocusInvestigation handles case insensitive searches correctly: routine-table 1`] = `Array []`; - -exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans: case-triggered-table 1`] = ` +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans 1`] = ` Array [ Object { "canton": "Chadiza", @@ -49,7 +47,7 @@ Array [ "caseNotificationDate": "2019-06-18", "district": null, "focusArea": "NVI_439", - "id": "plan-id-23", + "id": "plan-25", "jurisdiction_depth": 2, "jurisdiction_id": "450fc15b-5bd2-468a-927a-49cb10d3bcac", "jurisdiction_name": "NVI_439", @@ -70,7 +68,7 @@ Array [ "plan_id": "10f9e9fa-ce34-4b27-a961-72fab5206ab6", "plan_intervention_type": "FI", "plan_status": "active", - "plan_title": "Random plan", + "plan_title": "Test by Jane Doe", "plan_version": "1", "province": null, "reason": "Case Triggered", @@ -80,48 +78,7 @@ Array [ ] `; -exports[`containers/pages/ActiveFocusInvestigation handles search correctly for case triggered plans: routine-table 1`] = `Array []`; - -exports[`containers/pages/ActiveFocusInvestigation handles search correctly for routine plans: case-triggered-table 1`] = ` -Array [ - Object { - "canton": "Canton Tha Luang", - "caseClassification": null, - "caseNotificationDate": "2019-06-18", - "district": null, - "focusArea": "TLv1_02", - "id": "plan-id-2", - "jurisdiction_depth": 2, - "jurisdiction_id": "3378", - "jurisdiction_name": "TLv1_02", - "jurisdiction_name_path": Array [ - "Canton Tha Luang", - "Tha Luang Village", - ], - "jurisdiction_parent_id": "2977", - "jurisdiction_path": Array [ - "2989", - "2977", - ], - "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": "plan-id-2", - "plan_intervention_type": "IRS", - "plan_status": "active", - "plan_title": "A1-Tha Luang Village 1 Focus 01", - "plan_version": "1", - "province": null, - "reason": "Case Triggered", - "status": "A1", - "village": "Tha Luang Village", - }, -] -`; - -exports[`containers/pages/ActiveFocusInvestigation handles search correctly for routine plans: routine-table 1`] = ` +exports[`containers/pages/ActiveFocusInvestigation handles search correctly for routine plans 1`] = ` Array [ Object { "canton": "Chadiza", diff --git a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx index 8e3ec272a7..e91f80329d 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx +++ b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx @@ -11,7 +11,11 @@ import { Provider } from 'react-redux'; import { Router } from 'react-router'; import { CURRENT_FOCUS_INVESTIGATION } from '../../../../../configs/lang'; 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 +29,7 @@ jest.mock('../../../../../configs/env'); describe('containers/pages/ActiveFocusInvestigation', () => { beforeEach(() => { jest.resetAllMocks(); + store.dispatch(removePlansAction); MockDate.reset(); }); @@ -232,25 +237,23 @@ describe('containers/pages/ActiveFocusInvestigation', () => { }); it('handles search correctly for case triggered plans', async () => { + store.dispatch(fetchPlans([fixtures.plan24, fixtures.plan25])); + const mock: any = jest.fn(); const props = { - caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], - fetchPlansActionCreator: jest.fn(), history, location: mock, match: mock, - routinePlans: [fixtures.plan1], supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'random' } }); + wrapper.find('Input').simulate('change', { target: { value: 'Jane' } }); // Wait for debounce await new Promise(r => setTimeout(r, 1000)); wrapper.update(); @@ -259,30 +262,25 @@ describe('containers/pages/ActiveFocusInvestigation', () => { .find('ReactTable') .at(0) .prop('data') - ).toMatchSnapshot('case-triggered-table'); - expect( - wrapper - .find('ReactTable') - .at(1) - .prop('data') - ).toMatchSnapshot('routine-table'); + ).toMatchSnapshot(); }); it('handles search correctly for routine plans', async () => { + store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22])); + const mock: any = jest.fn(); const props = { - caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], - fetchPlansActionCreator: jest.fn(), history, location: mock, match: mock, - routinePlans: [fixtures.plan1], supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') @@ -291,80 +289,73 @@ describe('containers/pages/ActiveFocusInvestigation', () => { // Wait for debounce await new Promise(r => setTimeout(r, 1000)); wrapper.update(); - expect( - wrapper - .find('ReactTable') - .at(0) - .prop('data') - ).toMatchSnapshot('case-triggered-table'); expect( wrapper .find('ReactTable') .at(1) .prop('data') - ).toMatchSnapshot('routine-table'); + ).toMatchSnapshot(); + wrapper.unmount(); }); it('handles case insensitive searches correctly', async () => { + store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22])); + const mock: any = jest.fn(); const props = { - caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], - fetchPlansActionCreator: jest.fn(), history, location: mock, match: mock, - routinePlans: [fixtures.plan1], supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') .at(0) - .simulate('change', { target: { value: 'RANDOM' } }); + .simulate('change', { target: { value: 'LUANG' } }); // Wait for debounce await new Promise(r => setTimeout(r, 1000)); wrapper.update(); - expect( - wrapper - .find('ReactTable') - .at(0) - .prop('data') - ).toMatchSnapshot('case-triggered-table'); expect( wrapper .find('ReactTable') .at(1) .prop('data') - ).toMatchSnapshot('routine-table'); + ).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 mock: any = jest.fn(); const props = { - caseTriggeredPlans: [fixtures.plan2, fixtures.plan23], - fetchPlansActionCreator: jest.fn(), history, location: mock, match: mock, - routinePlans: [fixtures.plan1], supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') .at(0) - .simulate('change', { target: { value: 'kajkajakjTTyaa' } }); + .simulate('change', { target: { value: 'UUOAIO' } }); // Wait for debounce await new Promise(r => setTimeout(r, 1000)); wrapper.update(); + wrapper.update(); expect( wrapper .find('ReactTable') diff --git a/src/store/ducks/plans.ts b/src/store/ducks/plans.ts index c7948ff6d5..986e4f61bf 100644 --- a/src/store/ducks/plans.ts +++ b/src/store/ducks/plans.ts @@ -1,11 +1,12 @@ 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'; import { AnyAction, Store } from 'redux'; import { createSelector } from 'reselect'; import SeamlessImmutable from 'seamless-immutable'; import { FIReasonType, FIStatusType } from '../../components/forms/PlanForm/types'; -import { descendingOrderSort, FlexObject, removeNullJurisdictionPlans } from '../../helpers/utils'; +import { descendingOrderSort, removeNullJurisdictionPlans } from '../../helpers/utils'; /** the reducer name */ export const reducerName = 'plans'; @@ -104,13 +105,13 @@ export enum PlanEventType { export interface PlanEventPayload { baseEntityId: string; dateCreated: string; - details: FlexObject; + details: Dictionary; duration: number; entityType: InterventionType; eventDate: string; eventType: PlanEventType; formSubmissionId: string; - identifiers: FlexObject; + identifiers: Dictionary; obs: Array<{ fieldType: 'concept'; fieldDataType: string; @@ -551,12 +552,12 @@ export const makePlansArraySelector = (planKey?: string, sortField?: string) => 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 95d6aff121..6e95d28567 100644 --- a/src/store/ducks/tests/fixtures.ts +++ b/src/store/ducks/tests/fixtures.ts @@ -45,6 +45,26 @@ 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 plan23: Plan = { id: 'plan-id-23', jurisdiction_depth: 2, @@ -65,6 +85,46 @@ export const plan23: Plan = { 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, From 6ed03c05bc781a87b329d94d331765a41baa295d Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Fri, 17 Apr 2020 20:28:10 +0300 Subject: [PATCH 08/30] Refactor code --- .../pages/FocusInvestigation/active/index.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index 8985d43214..17463c5b06 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -147,26 +147,22 @@ class ActiveFocusInvestigation extends React.Component< } public debouncedSearch(event: React.ChangeEvent) { - this.setState({ search: event.target.value }, () => { - this.setState({ - searchedCaseTriggeredPlans: makePlansArraySelector()(store.getState(), { - interventionType: InterventionType.FI, - parentJurisdictionId: this.props.jurisdictionParentId, - reason: CASE_TRIGGERED, - statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - title: event.target.value, - }), - }); - - this.setState({ - searchedRoutinePlans: makePlansArraySelector()(store.getState(), { - interventionType: InterventionType.FI, - parentJurisdictionId: this.props.jurisdictionParentId, - reason: ROUTINE, - statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - title: event.target.value, - }), - }); + this.setState({ + search: event.target.value, + searchedCaseTriggeredPlans: makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: this.props.jurisdictionParentId, + reason: CASE_TRIGGERED, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: event.target.value, + }), + searchedRoutinePlans: makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: this.props.jurisdictionParentId, + reason: ROUTINE, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: event.target.value, + }), }); } From c9050094d02f3f31f2affa5d5a18cc976a516ab2 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Fri, 17 Apr 2020 20:54:32 +0300 Subject: [PATCH 09/30] Refactor code to enhance performance --- src/containers/pages/IRS/plans/index.tsx | 6 ++---- .../pages/InterventionPlan/PlanDefinitionList/index.tsx | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index 50c9207cba..dfae68776d 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -88,12 +88,10 @@ const IRSPlansList = (props: PlanListProps) => { }, []); useEffect(() => { - if (plans) { + if (plans && debouncedSearchQuery) { setSearchedPlans( plans.filter((plan: IRSPlan) => - debouncedSearchQuery - ? plan.plan_title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) - : true + plan.plan_title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) ) ); } diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx index 5e62d0bf08..6372adaafc 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -83,12 +83,10 @@ const PlanDefinitionList = (props: PlanListProps) => { }, []); useEffect(() => { - if (plans) { + if (plans && debouncedSearchQuery) { setSearchedPlans( plans.filter((plan: PlanDefinition) => - debouncedSearchQuery - ? plan.name.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) - : true + plan.name.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) ) ); } From a94c5deb762503df8ee6e194add09f91aad06e4f Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Fri, 17 Apr 2020 20:58:38 +0300 Subject: [PATCH 10/30] Refactor code --- .../pages/FocusInvestigation/active/index.tsx | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index 17463c5b06..e269eb1a66 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -149,21 +149,26 @@ class ActiveFocusInvestigation extends React.Component< public debouncedSearch(event: React.ChangeEvent) { this.setState({ search: event.target.value, - searchedCaseTriggeredPlans: makePlansArraySelector()(store.getState(), { - interventionType: InterventionType.FI, - parentJurisdictionId: this.props.jurisdictionParentId, - reason: CASE_TRIGGERED, - statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - title: event.target.value, - }), - searchedRoutinePlans: makePlansArraySelector()(store.getState(), { - interventionType: InterventionType.FI, - parentJurisdictionId: this.props.jurisdictionParentId, - reason: ROUTINE, - statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - title: event.target.value, - }), }); + + if (event.target.value) { + this.setState({ + searchedCaseTriggeredPlans: makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: this.props.jurisdictionParentId, + reason: CASE_TRIGGERED, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: event.target.value, + }), + searchedRoutinePlans: makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: this.props.jurisdictionParentId, + reason: ROUTINE, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: event.target.value, + }), + }); + } } public handleSearchChange(event: React.ChangeEvent) { From c36d5c1627f0b25c5a9c5d07ab0e424b395b7613 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Fri, 17 Apr 2020 21:06:45 +0300 Subject: [PATCH 11/30] Search plan list by title instead of name --- .../pages/InterventionPlan/PlanDefinitionList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx index 6372adaafc..64b64a303a 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -86,7 +86,7 @@ const PlanDefinitionList = (props: PlanListProps) => { if (plans && debouncedSearchQuery) { setSearchedPlans( plans.filter((plan: PlanDefinition) => - plan.name.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) + plan.title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) ) ); } From e137047b4947f1fb7593fc1fccc3678d88dcf344 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Mon, 20 Apr 2020 15:18:04 +0300 Subject: [PATCH 12/30] Add selector getPlanDefinitionsArrayByTitle --- .../PlanDefinitionList/index.tsx | 9 +++-- .../PlanDefinitionList/tests/index.test.tsx | 35 +++++++++++++------ .../ducks/opensrp/PlanDefinition/index.ts | 34 ++++++++++++++++++ .../PlanDefinition/tests/index.test.ts | 16 +++++++++ 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx index 64b64a303a..57e04746a5 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -24,12 +24,13 @@ import { HOME_URL, OPENSRP_PLANS, PLAN_LIST_URL, PLAN_UPDATE_URL } from '../../. import { displayError } from '../../../../helpers/errors'; import { useDebounce } from '../../../../helpers/hooks'; import { OpenSRPService } from '../../../../services/opensrp'; +import store from '../../../../store'; import planDefinitionReducer, { fetchPlanDefinitions, getPlanDefinitionsArray, + getPlanDefinitionsArrayByTitle, reducerName as planDefinitionReducerName, } from '../../../../store/ducks/opensrp/PlanDefinition'; - /** register the plan definitions reducer */ reducerRegistry.register(planDefinitionReducerName, planDefinitionReducer); @@ -83,11 +84,9 @@ const PlanDefinitionList = (props: PlanListProps) => { }, []); useEffect(() => { - if (plans && debouncedSearchQuery) { + if (debouncedSearchQuery) { setSearchedPlans( - plans.filter((plan: PlanDefinition) => - plan.title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) - ) + getPlanDefinitionsArrayByTitle()(store.getState(), { title: debouncedSearchQuery }) ); } }, [debouncedSearchQuery]); diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index 52af2461fb..0c1fa7b9e1 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -2,8 +2,11 @@ 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 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 */ @@ -42,15 +45,19 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { }); it('handles search correctly', async () => { + store.dispatch(fetchPlanDefinitions(fixtures.plans)); + const props = { fetchPlans: jest.fn(), plans: fixtures.plans, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') @@ -69,15 +76,19 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { }); it('handles a case insensitive search', async () => { + store.dispatch(fetchPlanDefinitions(fixtures.plans)); + const props = { fetchPlans: jest.fn(), plans: fixtures.plans, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') @@ -96,15 +107,19 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { }); it('renders empty table if no search matches', async () => { + store.dispatch(fetchPlanDefinitions(fixtures.plans)); + const props = { fetchPlans: jest.fn(), plans: fixtures.plans, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') diff --git a/src/store/ducks/opensrp/PlanDefinition/index.ts b/src/store/ducks/opensrp/PlanDefinition/index.ts index 0acf63ce1c..2b47c0f179 100644 --- a/src/store/ducks/opensrp/PlanDefinition/index.ts +++ b/src/store/ducks/opensrp/PlanDefinition/index.ts @@ -1,5 +1,7 @@ +import { Registry } from '@onaio/redux-reducer-registry'; 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,35 @@ 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: Registry +): 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 = (_: Registry, 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 + ); diff --git a/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts b/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts index dd1fc751e7..1de5360ef9 100644 --- a/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts +++ b/src/store/ducks/opensrp/PlanDefinition/tests/index.test.ts @@ -9,6 +9,7 @@ import reducer, { fetchPlanDefinitions, getPlanDefinitionById, getPlanDefinitionsArray, + getPlanDefinitionsArrayByTitle, getPlanDefinitionsById, reducerName, removePlanDefinitions, @@ -23,6 +24,7 @@ describe('reducers/opensrp/PlanDefinition', () => { beforeEach(() => { flushThunks = FlushThunks.createMiddleware(); jest.resetAllMocks(); + store.dispatch(removePlanDefinitions()); }); it('should have initial state', () => { @@ -60,6 +62,20 @@ 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', + }; + expect(getPlanDefinitionsArrayByTitle()(store.getState(), titleFilter)).toEqual([ + fixtures.plans[3], + ]); + expect(getPlanDefinitionsArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([ + fixtures.plans[3], + ]); + // reset store.dispatch(removePlanDefinitions()); expect(getPlanDefinitionsArray(store.getState())).toEqual([]); From 030fe13c77a6a1567b1fd3fbe7b7bc52033c323d Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Mon, 20 Apr 2020 15:24:34 +0300 Subject: [PATCH 13/30] Remove plan23 fixture from src/store/ducks/tests/fixtures.ts --- src/store/ducks/tests/fixtures.ts | 20 -------------------- src/store/ducks/tests/plans.test.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/store/ducks/tests/fixtures.ts b/src/store/ducks/tests/fixtures.ts index 6e95d28567..8695109266 100644 --- a/src/store/ducks/tests/fixtures.ts +++ b/src/store/ducks/tests/fixtures.ts @@ -65,26 +65,6 @@ export const plan22: Plan = { plan_version: '1', }; -export const plan23: Plan = { - id: 'plan-id-23', - 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: 'Random plan', - plan_version: '1', -}; - export const plan24: Plan = { id: 'plan-24', jurisdiction_depth: 2, diff --git a/src/store/ducks/tests/plans.test.ts b/src/store/ducks/tests/plans.test.ts index f7bfecba5a..5f63d4c261 100644 --- a/src/store/ducks/tests/plans.test.ts +++ b/src/store/ducks/tests/plans.test.ts @@ -58,11 +58,11 @@ describe('reducers/plans', () => { }); it('should fetch Plans', () => { - store.dispatch(fetchPlans([...fixtures.plans, fixtures.plan23])); + store.dispatch(fetchPlans([...fixtures.plans, fixtures.plan22])); const plansArraySelector = makePlansArraySelector(); - const allPlans = keyBy([...fixtures.plans, fixtures.plan23], (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, @@ -89,7 +89,7 @@ describe('reducers/plans', () => { expect(getPlansIdArray(store.getState())).toEqual([ 'ed2b4b7c-3388-53d9-b9f6-6a19d1ffde1f', 'plan-id-2', - 'plan-id-23', + 'plan-22', ]); expect(getPlansIdArray(store.getState(), InterventionType.IRS)).toEqual(['plan-id-2']); @@ -147,10 +147,10 @@ describe('reducers/plans', () => { parentJurisdictionId: '2977', }; const titleFilter = { - title: 'Random', + title: 'John', }; const titleUpperFilter = { - title: 'RANDOM', + title: 'JOHN', }; expect(getPlansArrayByInterventionType()(store.getState(), {})).toEqual(values(allPlans)); @@ -167,8 +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.plan23]); - expect(getPlansArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([fixtures.plan23]); + expect(getPlansArrayByTitle()(store.getState(), titleFilter)).toEqual([fixtures.plan22]); + expect(getPlansArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([fixtures.plan22]); expect( plansArraySelector(store.getState(), { ...fiFilter, From bb14c8b2b2ad037694b3faa1dbb87d3ae4ec1184 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Mon, 20 Apr 2020 15:47:09 +0300 Subject: [PATCH 14/30] Add selector getIRSPlansArrayByTitle --- src/containers/pages/IRS/plans/index.tsx | 8 ++-- .../pages/IRS/plans/tests/index.test.tsx | 38 ++++++++++++------- .../PlanDefinitionList/tests/index.test.tsx | 3 -- src/store/ducks/generic/plans.ts | 34 +++++++++++++++++ src/store/ducks/generic/tests/plans.test.ts | 13 +++++++ 5 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index dfae68776d..d55d638564 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -24,9 +24,11 @@ import { HOME_URL, REPORT_IRS_PLAN_URL } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; import { useDebounce } from '../../../../helpers/hooks'; import supersetFetch from '../../../../services/superset'; +import store from '../../../../store'; import IRSPlansReducer, { fetchIRSPlans, getIRSPlansArray, + getIRSPlansArrayByTitle, IRSPlan, reducerName as IRSPlansReducerName, } from '../../../../store/ducks/generic/plans'; @@ -88,11 +90,9 @@ const IRSPlansList = (props: PlanListProps) => { }, []); useEffect(() => { - if (plans && debouncedSearchQuery) { + if (debouncedSearchQuery) { setSearchedPlans( - plans.filter((plan: IRSPlan) => - plan.plan_title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()) - ) + getIRSPlansArrayByTitle()(store.getState(), { plan_title: debouncedSearchQuery }) ); } }, [debouncedSearchQuery]); diff --git a/src/containers/pages/IRS/plans/tests/index.test.tsx b/src/containers/pages/IRS/plans/tests/index.test.tsx index e8e48d04c2..af086da814 100644 --- a/src/containers/pages/IRS/plans/tests/index.test.tsx +++ b/src/containers/pages/IRS/plans/tests/index.test.tsx @@ -2,9 +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 { IRSPlansList } from '../'; +import ConnectedIRSPlansList, { IRSPlansList } from '../'; +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 */ @@ -46,15 +49,18 @@ describe('components/IRS Reports/IRSPlansList', () => { }); it('handles search correctly', async () => { + store.dispatch(fetchIRSPlans(fixtures.plans as IRSPlan[])); + const props = { fetchPlans: jest.fn(), - plans: fixtures.plans as IRSPlan[], service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') @@ -73,15 +79,18 @@ describe('components/IRS Reports/IRSPlansList', () => { }); it('handles a case insensitive search', async () => { + store.dispatch(fetchIRSPlans(fixtures.plans as IRSPlan[])); + const props = { fetchPlans: jest.fn(), - plans: fixtures.plans as IRSPlan[], service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') @@ -100,15 +109,18 @@ describe('components/IRS Reports/IRSPlansList', () => { }); it('renders empty table if no search matches', async () => { + store.dispatch(fetchIRSPlans(fixtures.plans as IRSPlan[])); + const props = { fetchPlans: jest.fn(), - plans: fixtures.plans as IRSPlan[], service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( - - - + + + + + ); wrapper .find('Input') diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index 0c1fa7b9e1..d8e08163f9 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -49,7 +49,6 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { const props = { fetchPlans: jest.fn(), - plans: fixtures.plans, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -80,7 +79,6 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { const props = { fetchPlans: jest.fn(), - plans: fixtures.plans, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -111,7 +109,6 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { const props = { fetchPlans: jest.fn(), - plans: fixtures.plans, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( diff --git a/src/store/ducks/generic/plans.ts b/src/store/ducks/generic/plans.ts index 6df4162fb5..bc63fc1c63 100644 --- a/src/store/ducks/generic/plans.ts +++ b/src/store/ducks/generic/plans.ts @@ -1,5 +1,7 @@ +import { Registry } from '@onaio/redux-reducer-registry'; 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,35 @@ 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: Registry): 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 = (_: Registry, props: IRSPlanFilters) => props.plan_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 getIRSPlansArrayByTitle = (planKey?: string) => + createSelector([IRSPlansArrayBaseSelector(planKey), getTitle], (plans, title) => + title + ? plans.filter(plan => plan.plan_title.toLowerCase().includes(title.toLowerCase())) + : plans + ); diff --git a/src/store/ducks/generic/tests/plans.test.ts b/src/store/ducks/generic/tests/plans.test.ts index 4dfad2d146..0a8ebecbe5 100644 --- a/src/store/ducks/generic/tests/plans.test.ts +++ b/src/store/ducks/generic/tests/plans.test.ts @@ -8,6 +8,7 @@ import reducer, { fetchIRSPlans, getIRSPlanById, getIRSPlansArray, + getIRSPlansArrayByTitle, getIRSPlansById, IRSPlan, reducerName, @@ -56,6 +57,18 @@ 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', + }; + expect(getIRSPlansArrayByTitle()(store.getState(), titleFilter)).toEqual([fixtures.plans[2]]); + expect(getIRSPlansArrayByTitle()(store.getState(), titleUpperFilter)).toEqual([ + fixtures.plans[2], + ]); + // reset store.dispatch(removeIRSPlans()); expect(getIRSPlansArray(store.getState())).toEqual([]); From fe127c2adc07b4e789f7e661878a372aa746d2e7 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Mon, 20 Apr 2020 20:36:31 +0300 Subject: [PATCH 15/30] Move selectors in ActiveFocusInvestigation to mapStateToProps Push the searched value as a query param to URL then use that value in selector to filter by title --- .../pages/FocusInvestigation/active/index.tsx | 114 +++++++++--------- 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index e269eb1a66..1dac5ed988 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -5,6 +5,8 @@ import reducerRegistry from '@onaio/redux-reducer-registry'; import superset from '@onaio/superset-connector'; import { Dictionary } from '@onaio/utils'; import _ from 'lodash'; +import { trimStart } from 'lodash'; +import querystring from 'querystring'; import * as React from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; @@ -98,13 +100,7 @@ export interface ActiveFIProps { supersetService: typeof supersetFetch; plan: Plan | null; jurisdictionParentId: string; -} - -/** Interface defining component state */ -export interface ActiveFIState { - search?: string; - searchedCaseTriggeredPlans: Plan[]; - searchedRoutinePlans: Plan[]; + searchedTitle: string | null; } /** default props for ActiveFI component */ @@ -114,24 +110,20 @@ export const defaultActiveFIProps: ActiveFIProps = { jurisdictionParentId: '', plan: null, routinePlans: null, + searchedTitle: null, supersetService: supersetFetch, }; /** Reporting for Active Focus Investigations */ class ActiveFocusInvestigation extends React.Component< ActiveFIProps & RouteComponentProps, - ActiveFIState + {} > { public static defaultProps: ActiveFIProps = defaultActiveFIProps; public debouncedSearchCallback = _.debounce(this.debouncedSearch, 1000); constructor(props: ActiveFIProps & RouteComponentProps) { super(props); - this.state = { - search: '', - searchedCaseTriggeredPlans: [], - searchedRoutinePlans: [], - }; this.handleSearchChange = this.handleSearchChange.bind(this); this.debouncedSearchCallback = _.debounce(this.debouncedSearch, 1000); } @@ -147,28 +139,10 @@ class ActiveFocusInvestigation extends React.Component< } public debouncedSearch(event: React.ChangeEvent) { - this.setState({ - search: event.target.value, + this.props.history.push({ + pathname: this.props.location.pathname, + search: `?search=${event.target.value}`, }); - - if (event.target.value) { - this.setState({ - searchedCaseTriggeredPlans: makePlansArraySelector()(store.getState(), { - interventionType: InterventionType.FI, - parentJurisdictionId: this.props.jurisdictionParentId, - reason: CASE_TRIGGERED, - statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - title: event.target.value, - }), - searchedRoutinePlans: makePlansArraySelector()(store.getState(), { - interventionType: InterventionType.FI, - parentJurisdictionId: this.props.jurisdictionParentId, - reason: ROUTINE, - statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], - title: event.target.value, - }), - }); - } } public handleSearchChange(event: React.ChangeEvent) { @@ -193,8 +167,7 @@ class ActiveFocusInvestigation extends React.Component< url: HOME_URL, }; - const { caseTriggeredPlans, routinePlans, plan } = this.props; - const { searchedCaseTriggeredPlans, searchedRoutinePlans, search } = this.state; + const { caseTriggeredPlans, routinePlans, plan, searchedTitle } = this.props; // We need to initialize jurisdictionName to a falsy value let jurisdictionName = null; @@ -224,7 +197,8 @@ class ActiveFocusInvestigation extends React.Component< caseTriggeredPlans && caseTriggeredPlans.length === 0 && routinePlans && - routinePlans.length === 0 + routinePlans.length === 0 && + searchedTitle === null ) { return ; } @@ -241,10 +215,7 @@ class ActiveFocusInvestigation extends React.Component<

{pageTitle}


- {[ - search ? searchedCaseTriggeredPlans : caseTriggeredPlans, - search ? searchedRoutinePlans : routinePlans, - ].forEach((plansArray: Plan[] | null, i) => { + {[caseTriggeredPlans, routinePlans].forEach((plansArray: Plan[] | null, i) => { const locationColumns: Column[] = getLocationColumns(locationHierarchy, true); if (plansArray && plansArray.length) { const jurisdictionValidPlans = removeNullJurisdictionPlans(plansArray); @@ -461,10 +432,10 @@ export { ActiveFocusInvestigation }; /** interface to describe props from mapStateToProps */ interface DispatchedStateProps { - jurisdictionParentId: string; plan: Plan | null; caseTriggeredPlans: Plan[] | null; routinePlans: Plan[] | null; + searchedTitle: string; } /** map state to props */ @@ -476,27 +447,52 @@ 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 searchString = trimStart(ownProps.location.search, '?'); + const queryParams = querystring.parse(searchString); + const searchedTitle = queryParams.search as string; + let caseTriggeredPlans = []; + let routinePlans = []; + + if (searchedTitle) { + caseTriggeredPlans = makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: jurisdictionParentId, + reason: CASE_TRIGGERED, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: searchedTitle, + }); + routinePlans = makePlansArraySelector()(store.getState(), { + interventionType: InterventionType.FI, + parentJurisdictionId: jurisdictionParentId, + reason: ROUTINE, + statusList: [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + title: searchedTitle, + }); + } else { + caseTriggeredPlans = getPlansArray( + state, + InterventionType.FI, + [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + CASE_TRIGGERED, + [], + jurisdictionParentId + ); + routinePlans = getPlansArray( + state, + InterventionType.FI, + [PlanStatus.ACTIVE, PlanStatus.COMPLETE], + ROUTINE, + [], + jurisdictionParentId + ); + } + return { caseTriggeredPlans, - jurisdictionParentId, plan, routinePlans, + searchedTitle, }; }; From e57931853419802fa51790c54191f68710c10f31 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 21 Apr 2020 13:59:18 +0300 Subject: [PATCH 16/30] Move search handle submit and handle change to component SearchForm Have onSubmit push the query string to URL. Remove useDebounce util function since we are now updating the query string on submit rather than on input change. Update components to use the enhanced SearchForm --- src/components/forms/Search/index.tsx | 43 +- .../tests/__snapshots__/index.test.tsx.snap | 39 +- .../forms/Search/tests/index.test.tsx | 17 + .../pages/FocusInvestigation/active/index.tsx | 16 +- .../active/tests/index.test.tsx | 79 +- src/containers/pages/IRS/plans/index.tsx | 41 +- .../pages/IRS/plans/tests/index.test.tsx | 81 +- .../PlanDefinitionList/index.tsx | 37 +- .../tests/__snapshots__/index.test.tsx.snap | 2733 +++++++++-------- .../PlanDefinitionList/tests/index.test.tsx | 85 +- src/helpers/hooks.tsx | 32 - 11 files changed, 1678 insertions(+), 1525 deletions(-) diff --git a/src/components/forms/Search/index.tsx b/src/components/forms/Search/index.tsx index b25f9fcea7..d25ddce43d 100644 --- a/src/components/forms/Search/index.tsx +++ b/src/components/forms/Search/index.tsx @@ -1,19 +1,48 @@ -import React from 'react'; -import { Form, FormGroup, Input } from 'reactstrap'; +import { History } from 'history'; +import React, { useState } from 'react'; +import { Button, Form, FormGroup, Input } from 'reactstrap'; +import { SEARCH } from '../../../configs/lang'; -export type SearchChange = (event: React.ChangeEvent) => void; +/** + * Interface for handleSearchChange event handler + */ +export type Change = (event: React.ChangeEvent) => void; -/** Interface for SearchForm props */ +/** + * Interface for handleSubmit event handler + */ +export type Submit = (event: React.FormEvent) => void; + +/** + * Interface for SearchForm props + */ export interface SearchFormProps { - handleSearchChange: SearchChange; + history: History; } +/** Search Form component */ export const SearchForm = (props: SearchFormProps) => { + const [searchQuery, setSearchQuery] = useState(''); + + const handleSearchChange: Change = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + }; + + const handleSubmit: Submit = (event: React.FormEvent) => { + event.preventDefault(); + props.history.push({ + search: `?search=${searchQuery}`, + }); + }; + return ( -
+ - + +
); }; diff --git a/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap b/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap index bbcec553c3..b5badc20ec 100644 --- a/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap +++ b/src/components/forms/Search/tests/__snapshots__/index.test.tsx.snap @@ -3,13 +3,35 @@ 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 index f7a73d9220..b937244b14 100644 --- a/src/components/forms/Search/tests/index.test.tsx +++ b/src/components/forms/Search/tests/index.test.tsx @@ -1,15 +1,32 @@ 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, }; const wrapper = mount(); expect(toJson(wrapper)).toMatchSnapshot(); wrapper.unmount(); }); + + it('handles submit correctly', () => { + jest.spyOn(history, 'push'); + const props = { + handleSearchChange: jest.fn(), + history, + }; + const wrapper = mount(); + wrapper.find('Input').simulate('change', { target: { value: 'test' } }); + wrapper.find('Form').simulate('submit'); + expect(history.push).toBeCalledWith({ search: '?search=test' }); + wrapper.unmount(); + }); }); diff --git a/src/containers/pages/FocusInvestigation/active/index.tsx b/src/containers/pages/FocusInvestigation/active/index.tsx index 1dac5ed988..d708f11644 100644 --- a/src/containers/pages/FocusInvestigation/active/index.tsx +++ b/src/containers/pages/FocusInvestigation/active/index.tsx @@ -121,11 +121,8 @@ class ActiveFocusInvestigation extends React.Component< > { public static defaultProps: ActiveFIProps = defaultActiveFIProps; - public debouncedSearchCallback = _.debounce(this.debouncedSearch, 1000); constructor(props: ActiveFIProps & RouteComponentProps) { super(props); - this.handleSearchChange = this.handleSearchChange.bind(this); - this.debouncedSearchCallback = _.debounce(this.debouncedSearch, 1000); } public componentDidMount() { @@ -138,17 +135,6 @@ class ActiveFocusInvestigation extends React.Component< .catch(err => displayError(err)); } - public debouncedSearch(event: React.ChangeEvent) { - this.props.history.push({ - pathname: this.props.location.pathname, - search: `?search=${event.target.value}`, - }); - } - - public handleSearchChange(event: React.ChangeEvent) { - event.persist(); // This will ensure that the event is not pooled for more details https://reactjs.org/docs/events.html - this.debouncedSearchCallback(event); - } public render() { const breadcrumbProps: BreadCrumbProps = { currentPage: { @@ -214,7 +200,7 @@ class ActiveFocusInvestigation extends React.Component<

{pageTitle}


- + {[caseTriggeredPlans, routinePlans].forEach((plansArray: Plan[] | null, i) => { const locationColumns: Column[] = getLocationColumns(locationHierarchy, true); if (plansArray && plansArray.length) { diff --git a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx index e91f80329d..c3e8b69df2 100644 --- a/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx +++ b/src/containers/pages/FocusInvestigation/active/tests/index.test.tsx @@ -10,6 +10,7 @@ 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, @@ -239,11 +240,18 @@ describe('containers/pages/ActiveFocusInvestigation', () => { it('handles search correctly for case triggered plans', async () => { store.dispatch(fetchPlans([fixtures.plan24, fixtures.plan25])); - const mock: any = jest.fn(); const props = { history, - location: mock, - match: mock, + location: { + pathname: FI_URL, + search: '?search=Jane', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -253,10 +261,6 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ); - wrapper.find('Input').simulate('change', { target: { value: 'Jane' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1000)); - wrapper.update(); expect( wrapper .find('ReactTable') @@ -268,11 +272,18 @@ describe('containers/pages/ActiveFocusInvestigation', () => { it('handles search correctly for routine plans', async () => { store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22])); - const mock: any = jest.fn(); const props = { history, - location: mock, - match: mock, + location: { + pathname: FI_URL, + search: '?search=Luang', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -282,13 +293,6 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'Luang' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1000)); - wrapper.update(); expect( wrapper .find('ReactTable') @@ -301,11 +305,18 @@ describe('containers/pages/ActiveFocusInvestigation', () => { it('handles case insensitive searches correctly', async () => { store.dispatch(fetchPlans([fixtures.plan1, fixtures.plan22])); - const mock: any = jest.fn(); const props = { history, - location: mock, - match: mock, + location: { + pathname: FI_URL, + search: '?search=LUANG', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -315,13 +326,6 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'LUANG' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1000)); - wrapper.update(); expect( wrapper .find('ReactTable') @@ -334,11 +338,18 @@ describe('containers/pages/ActiveFocusInvestigation', () => { 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 mock: any = jest.fn(); const props = { history, - location: mock, - match: mock, + location: { + pathname: FI_URL, + search: '?search=Amazon', + }, + match: { + isExact: true, + params: {}, + path: `${FI_URL}`, + url: `${FI_URL}`, + }, supersetService: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -348,14 +359,6 @@ describe('containers/pages/ActiveFocusInvestigation', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'UUOAIO' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1000)); - wrapper.update(); - wrapper.update(); expect( wrapper .find('ReactTable') diff --git a/src/containers/pages/IRS/plans/index.tsx b/src/containers/pages/IRS/plans/index.tsx index d55d638564..88bc9992c7 100644 --- a/src/containers/pages/IRS/plans/index.tsx +++ b/src/containers/pages/IRS/plans/index.tsx @@ -1,8 +1,11 @@ import ListView from '@onaio/list-view'; import reducerRegistry from '@onaio/redux-reducer-registry'; +import { trimStart } from 'lodash'; +import querystring from 'querystring'; 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'; @@ -22,7 +25,6 @@ import { import { planStatusDisplay } from '../../../../configs/settings'; import { HOME_URL, REPORT_IRS_PLAN_URL } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; -import { useDebounce } from '../../../../helpers/hooks'; import supersetFetch from '../../../../services/superset'; import store from '../../../../store'; import IRSPlansReducer, { @@ -44,16 +46,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(false); - const [searchedPlans, setSearchedPlans] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); const pageTitle: string = IRS_PLANS; - // Return the latest search query if it's been more than 1s since - // it was last called - const debouncedSearchQuery = useDebounce(searchQuery); - const breadcrumbProps = { currentPage: { label: pageTitle, @@ -81,22 +77,10 @@ const IRSPlansList = (props: PlanListProps) => { } } - const handleSearchChange = (event: React.ChangeEvent) => { - setSearchQuery(event.target.value); - }; - useEffect(() => { loadData().catch(error => displayError(error)); }, []); - useEffect(() => { - if (debouncedSearchQuery) { - setSearchedPlans( - getIRSPlansArrayByTitle()(store.getState(), { plan_title: debouncedSearchQuery }) - ); - } - }, [debouncedSearchQuery]); - const listViewData = (planList: IRSPlan[]) => planList.map(planObj => { return [ @@ -115,7 +99,7 @@ const IRSPlansList = (props: PlanListProps) => { } const listViewProps = { - data: listViewData(searchQuery ? searchedPlans : plans), + data: listViewData(plans), headerItems: [TITLE, DATE_CREATED, START_DATE, END_DATE, STATUS_HEADER], tableClass: 'table table-bordered plans-list', }; @@ -132,7 +116,8 @@ const IRSPlansList = (props: PlanListProps) => {

- + + @@ -161,11 +146,17 @@ interface DispatchedStateProps { } /** map state to props */ -const mapStateToProps = (state: Partial): DispatchedStateProps => { - const planDefinitionsArray = getIRSPlansArray(state); +const mapStateToProps = (state: Partial, ownProps: any): DispatchedStateProps => { + const searchString = trimStart(ownProps.location.search, '?'); + const queryParams = querystring.parse(searchString); + + const searchedTitle = queryParams.search as string; + const IRSPlansArray = !searchedTitle + ? getIRSPlansArray(state) + : getIRSPlansArrayByTitle()(store.getState(), { 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 af086da814..659cf36cf1 100644 --- a/src/containers/pages/IRS/plans/tests/index.test.tsx +++ b/src/containers/pages/IRS/plans/tests/index.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; 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'; @@ -22,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( @@ -34,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( @@ -53,6 +80,17 @@ describe('components/IRS Reports/IRSPlansList', () => { const props = { fetchPlans: jest.fn(), + history, + location: { + pathname: REPORT_IRS_PLAN_URL, + search: '?search=Berg', + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}`, + url: `${REPORT_IRS_PLAN_URL}`, + }, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -62,13 +100,6 @@ describe('components/IRS Reports/IRSPlansList', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'Berg' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1500)); - wrapper.mount(); expect( wrapper .find('tbody tr td') @@ -83,6 +114,17 @@ describe('components/IRS Reports/IRSPlansList', () => { const props = { fetchPlans: jest.fn(), + history, + location: { + pathname: REPORT_IRS_PLAN_URL, + search: '?search=BERG', + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}`, + url: `${REPORT_IRS_PLAN_URL}`, + }, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -92,13 +134,6 @@ describe('components/IRS Reports/IRSPlansList', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'BERG' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1500)); - wrapper.mount(); expect( wrapper .find('tbody tr td') @@ -113,6 +148,17 @@ describe('components/IRS Reports/IRSPlansList', () => { const props = { fetchPlans: jest.fn(), + history, + location: { + pathname: REPORT_IRS_PLAN_URL, + search: '?search=Amazon', + }, + match: { + isExact: true, + params: {}, + path: `${REPORT_IRS_PLAN_URL}`, + url: `${REPORT_IRS_PLAN_URL}`, + }, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -122,13 +168,6 @@ describe('components/IRS Reports/IRSPlansList', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'OOOOOOOPPOAPOPAO' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1500)); - 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 57e04746a5..c8f3093242 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/index.tsx @@ -1,8 +1,11 @@ import ListView from '@onaio/list-view'; import reducerRegistry from '@onaio/redux-reducer-registry'; +import { trimStart } from 'lodash'; +import querystring from 'querystring'; 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'; @@ -22,7 +25,6 @@ import { import { PlanDefinition, planStatusDisplay } from '../../../../configs/settings'; import { HOME_URL, OPENSRP_PLANS, PLAN_LIST_URL, PLAN_UPDATE_URL } from '../../../../constants'; import { displayError } from '../../../../helpers/errors'; -import { useDebounce } from '../../../../helpers/hooks'; import { OpenSRPService } from '../../../../services/opensrp'; import store from '../../../../store'; import planDefinitionReducer, { @@ -42,17 +44,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 [searchedPlans, setSearchedPlans] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); const apiService = new service(OPENSRP_PLANS); const pageTitle: string = PLANS; - // Return the latest search query if it's been more than 1s since - // it was last called - const debouncedSearchQuery = useDebounce(searchQuery); - const breadcrumbProps = { currentPage: { label: pageTitle, @@ -83,18 +79,6 @@ const PlanDefinitionList = (props: PlanListProps) => { loadData().catch(err => displayError(err)); }, []); - useEffect(() => { - if (debouncedSearchQuery) { - setSearchedPlans( - getPlanDefinitionsArrayByTitle()(store.getState(), { title: debouncedSearchQuery }) - ); - } - }, [debouncedSearchQuery]); - - const handleSearchChange = (event: React.ChangeEvent) => { - setSearchQuery(event.target.value); - }; - const listViewData = (data: PlanDefinition[]) => data.map(planObj => { const typeUseContext = planObj.useContext.filter(e => e.code === 'interventionType'); @@ -114,7 +98,7 @@ const PlanDefinitionList = (props: PlanListProps) => { } const listViewProps = { - data: listViewData(searchQuery ? searchedPlans : plans), + data: listViewData(plans), headerItems: [TITLE, INTERVENTION_TYPE_LABEL, STATUS_HEADER, LAST_MODIFIED], tableClass: 'table table-bordered plans-list', }; @@ -137,7 +121,7 @@ const PlanDefinitionList = (props: PlanListProps) => {
- + @@ -166,8 +150,13 @@ interface DispatchedStateProps { } /** map state to props */ -const mapStateToProps = (state: Partial): DispatchedStateProps => { - const planDefinitionsArray = getPlanDefinitionsArray(state); +const mapStateToProps = (state: Partial, ownProps: any): DispatchedStateProps => { + const searchString = trimStart(ownProps.location.search, '?'); + const queryParams = querystring.parse(searchString); + const searchedTitle = queryParams.search as string; + const planDefinitionsArray = !searchedTitle + ? getPlanDefinitionsArray(state) + : getPlanDefinitionsArrayByTitle()(store.getState(), { 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 ef101907c2..03d172ab3b 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,7 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly 1`] = ` - - =", - "unit": "form(s)", - "value": 1, - }, + Object { + "code": "Larval Dipping", + "description": "Perform a minimum of three larval dipping activities in the operational area", + "goalId": "Larval_Dipping_Min_3_Sites", + "identifier": "2482dfd7-8284-43c6-bea1-a03dcda71ff4", + "prefix": 5, + "reason": "Investigation", + "subjectCodableConcept": Object { + "text": "Breeding_Site", + }, + "taskTemplate": "Larval_Dipping", + "timingPeriod": Object { + "end": "2019-05-28", + "start": "2019-05-21", + }, + "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": "423f6665-5367-40be-855e-7c5e6941a0c3", + "prefix": 6, + "reason": "Investigation", + "subjectCodableConcept": Object { + "text": "Mosquito_Collection_Point", + }, + "taskTemplate": "Mosquito_Collection_Point", + "timingPeriod": Object { + "end": "2019-05-28", + "start": "2019-05-21", + }, + "title": "Mosquito Collection", + }, + Object { + "code": "BCC", + "description": "Conduct BCC activity", + "goalId": "BCC_Focus", + "identifier": "c8fc89a9-cdd2-4746-8272-650883ae380e", + "prefix": 7, + "reason": "Investigation", + "subjectCodableConcept": Object { + "text": "Operational_Area", + }, + "taskTemplate": "BCC_Focus", + "timingPeriod": Object { + "end": "2019-06-21", + "start": "2019-05-21", + }, + "title": "Behaviour Change Communication", + }, + ], + "date": "2019-05-19", + "effectivePeriod": Object { + "end": "2019-08-30", + "start": "2019-05-20", + }, + "goal": Array [ + Object { + "description": "Confirm the index case", + "id": "Case_Confirmation", + "priority": "medium-priority", + "target": Array [ + Object { + "detail": Object { + "detailQuantity": Object { + "comparator": ">=", + "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-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 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-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": "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 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-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", + "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", }, - ], - "name": "A2-Lusaka_Akros_Focus_2", - "serverVersion": 1563303150422, - "status": "active", - "title": "A2-Lusaka Akros Test Focus 2", - "useContext": Array [ - Object { - "code": "interventionType", - "valueCodableConcept": "FI", + "taskTemplate": "Action1_Perform_BCC", + "timingPeriod": Object { + "end": "2019-07-30", + "start": "2019-07-10", }, - Object { - "code": "fiStatus", - "valueCodableConcept": "A2", + "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", }, - Object { - "code": "fiReason", - "valueCodableConcept": "Routine", + "taskTemplate": "Action2_Spray_Structures", + "timingPeriod": Object { + "end": "2019-07-30", + "start": "2019-07-10", }, - ], - "version": "1", + "title": "Spray Structures", + }, + ], + "date": "2019-07-10", + "effectivePeriod": Object { + "end": "2019-07-30", + "start": "2019-07-10", }, - 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", + "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", }, - "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, - }, + 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-10", - "measure": "Number of BCC communication activities that happened", }, - ], + "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", }, - 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", - }, - ], + "taskTemplate": "Case_Confirmation", + "timingPeriod": Object { + "end": "2019-07-28", + "start": "2019-07-18", }, - ], - "identifier": "8fa7eb32-99d7-4b49-8332-9ecedd6d51ae", - "jurisdiction": Array [ - Object { - "code": "35968df5-f335-44ae-8ae5-25804caa2d86", + "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", }, - Object { - "code": "3952", + "taskTemplate": "RACD_register_families", + "timingPeriod": Object { + "end": "2019-08-07", + "start": "2019-07-18", }, - Object { - "code": "ac7ba751-35e8-4b46-9e53-3cbaad193697", + "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", }, - ], - "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", + "taskTemplate": "RACD_Blood_Screening", + "timingPeriod": Object { + "end": "2019-08-07", + "start": "2019-07-18", }, - ], - "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", + "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", }, - 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", + "taskTemplate": "ITN_Visit_Structures", + "timingPeriod": Object { + "end": "2019-08-07", + "start": "2019-07-18", }, - 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", + "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", }, - 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", + "taskTemplate": "Larval_Dipping", + "timingPeriod": Object { + "end": "2019-08-07", + "start": "2019-07-18", }, - 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", + "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", }, - 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", + "taskTemplate": "Mosquito_Collection_Point", + "timingPeriod": Object { + "end": "2019-08-07", + "start": "2019-07-18", }, - 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", + "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", }, - ], - "date": "2019-07-18", - "effectivePeriod": Object { - "end": "2019-08-07", - "start": "2019-07-18", + "taskTemplate": "BCC_Focus", + "timingPeriod": Object { + "end": "2019-08-07", + "start": "2019-07-18", + }, + "title": "Behaviour Change Communication", }, - "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, - }, + ], + "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-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": "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": "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": "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 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 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", + "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", }, - 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", }, - "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", + ], + "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", }, - Object { - "code": "fiStatus", - "valueCodableConcept": "A2", + "taskTemplate": "RACD_register_families", + "timingPeriod": Object { + "end": "2019-12-31", + "start": "2019-10-18", }, - Object { - "code": "fiReason", - "valueCodableConcept": "Routine", + "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", }, - ], - }, - 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", + "taskTemplate": "MDA_Dispense", + "timingPeriod": Object { + "end": "2019-12-31", + "start": "2019-10-18", }, - 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", + "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", }, - 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", + "taskTemplate": "MDA_Adherence", + "timingPeriod": Object { + "end": "2019-12-31", + "start": "2019-10-18", }, - ], - "date": "2019-10-18", - "effectivePeriod": Object { - "end": "2019-12-31", - "start": "2019-10-18", + "title": "MDA Round 1 Adherence", }, - "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, - }, + ], + "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 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 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]} - > -
- +
+ + - - - - - + + + -
- +
+ +
- - + + + +
+
+
+ +
-
- -
-

- 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={ + + 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 [ - "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" + + 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", - ] - } + Title + + + + + + + + + - - A2-Lusaka Akros Test Focus 2 - - - - - - - - - , + "FI", + "active", + "2019-05-19", + ] + } > - - TwoTwoOne_01 IRS 2019-07-10 - , - "IRS", - "active", - "2019-07-10", - ] - } + key="0" > - + + + + + + + - - TwoTwoOne_01 IRS 2019-07-10 - - - - - - - - - , + "IRS", + "active", + "2019-07-10", + ] + } > - - A1 - TwoTwoTwo_03 - 2019-07-18 - , - "FI", - "active", - "2019-07-18", - ] - } + key="0" > - + + + + + + + - - A1 - TwoTwoTwo_03 - 2019-07-18 - - - - - - - - - , + "FI", + "active", + "2019-07-18", + ] + } > - - A Test By Mosh - , - "FI", - "draft", - "2019-05-19", - ] - } + key="0" > - + + + + + + + - - A Test By Mosh - - - - - - - - - , + "FI", + "draft", + "2019-05-19", + ] + } > - - Macepa MDA Campaign - , - "MDA", - "active", - "2019-10-18", - ] - } + key="0" > - + + + + + + + - - Macepa MDA Campaign - - - - - - - - - -
- Title - - Intervention Type - - Status - - Last Modified -
- + Intervention Type + + + Status + + Last Modified +
- FI - - active - - 2019-05-19 -
+ + A2-Lusaka Akros Test Focus 2 + + + + FI + + active + + 2019-05-19 +
- IRS - - active - - 2019-07-10 -
+ + TwoTwoOne_01 IRS 2019-07-10 + + + + IRS + + active + + 2019-07-10 +
- FI - - active - - 2019-07-18 -
+ + A1 - TwoTwoTwo_03 - 2019-07-18 + + + + FI + + active + + 2019-07-18 +
- FI - - draft - - 2019-05-19 -
+ + A Test By Mosh + + + + FI + + draft + + 2019-05-19 +
- MDA - - active - , + "MDA", + "active", + "2019-10-18", + ] + } + > + + - 2019-10-18 -
-
-
- -
- -
- - + + Macepa MDA Campaign + + + + + MDA + + + active + + + 2019-10-18 + + + + + + + + + +
+ + `; diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index d8e08163f9..ada140ced9 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; 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'; @@ -19,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( @@ -33,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( @@ -40,7 +72,7 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('PlanDefinitionList'))).toMatchSnapshot(); wrapper.unmount(); }); @@ -49,6 +81,17 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { const props = { fetchPlans: jest.fn(), + history, + location: { + pathname: PLAN_LIST_URL, + search: '?search=Mosh', + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}`, + url: `${PLAN_LIST_URL}`, + }, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -58,12 +101,6 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'Mosh' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect( wrapper @@ -79,6 +116,17 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { const props = { fetchPlans: jest.fn(), + history, + location: { + pathname: PLAN_LIST_URL, + search: '?search=MOSH', + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}`, + url: `${PLAN_LIST_URL}`, + }, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -88,12 +136,6 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'MOsh' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect( wrapper @@ -109,6 +151,17 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { const props = { fetchPlans: jest.fn(), + history, + location: { + pathname: PLAN_LIST_URL, + search: '?search=Amazon', + }, + match: { + isExact: true, + params: {}, + path: `${PLAN_LIST_URL}`, + url: `${PLAN_LIST_URL}`, + }, service: jest.fn().mockImplementationOnce(() => Promise.resolve([])), }; const wrapper = mount( @@ -118,12 +171,6 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - wrapper - .find('Input') - .at(0) - .simulate('change', { target: { value: 'OOOOOOOPPOAPOPAO' } }); - // Wait for debounce - await new Promise(r => setTimeout(r, 1500)); wrapper.mount(); expect(toJson(wrapper.find('tbody tr'))).toEqual(null); }); diff --git a/src/helpers/hooks.tsx b/src/helpers/hooks.tsx index 9f50dd4c01..3e4c1b433a 100644 --- a/src/helpers/hooks.tsx +++ b/src/helpers/hooks.tsx @@ -17,35 +17,3 @@ export function useConfirmOnBrowserUnload(hasUnsavedChanges: boolean = false) { return () => window.removeEventListener('beforeunload', callback); }); } - -/** - * Debounce calls to make sure they do not execute too frequently such as when - * a user is typing and a call needs to be made to the API - * @param value Value to be debounced - * @param delay Time in ms to wait for since the last call - */ -export function useDebounce(value: string, delay: number = 1000) { - // State and setters for debounced value - const [debouncedValue, setDebouncedValue] = React.useState(value); - - React.useEffect(() => { - // Set debouncedValue to value (passed in) after the specified delay - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - // Return a cleanup function that will be called every time ... - // ... useEffect is re-called. useEffect will only be re-called ... - // ... if value changes (see the inputs array below). - // This is how we prevent debouncedValue from changing if value is ... - // ... changed within the delay period. Timeout gets cleared and restarted. - // To put it in context, if the user is typing within our app's ... - // ... search box, we don't want the debouncedValue to update until ... - // ... they've stopped typing for more than 1000ms. - return () => { - clearTimeout(handler); - }; - }, [value]); - - return debouncedValue; -} From 28f8cf8b28430a71ef6d509d41b8eb76adbe1d8a Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Tue, 21 Apr 2020 14:20:57 +0300 Subject: [PATCH 17/30] Add placeholder prop for SearchForm. Update failing test snapshot --- src/components/forms/Search/index.tsx | 8 +++++++- src/components/forms/Search/tests/index.test.tsx | 11 +++++++++++ .../tests/__snapshots__/index.test.tsx.snap | 1 - 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/forms/Search/index.tsx b/src/components/forms/Search/index.tsx index d25ddce43d..2326f9ec86 100644 --- a/src/components/forms/Search/index.tsx +++ b/src/components/forms/Search/index.tsx @@ -18,6 +18,7 @@ export type Submit = (event: React.FormEvent) => void; */ export interface SearchFormProps { history: History; + placeholder?: string; } /** Search Form component */ @@ -38,7 +39,12 @@ export const SearchForm = (props: SearchFormProps) => { return (
- +
- - - -
- + + +
- -
- - 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 1`] = ` + +
+ + +
+ + + +
+
+ + +
+ +
`; diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index 7cc7c0f8a1..ee36a12c2a 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -72,7 +72,10 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - expect(toJson(wrapper.find('PlanDefinitionList'))).toMatchSnapshot(); + expect(toJson(wrapper.find('HeaderBreadcrumb'))).toMatchSnapshot('header bread crumb'); + expect(toJson(wrapper.find('Row').at(0))).toMatchSnapshot('row heading'); + expect(toJson(wrapper.find('SearchForm'))).toMatchSnapshot('search form'); + expect(wrapper.find('ListView').props()).toMatchSnapshot('list view data'); wrapper.unmount(); }); From 33a624bb3d2c934c9705a2ebdd8fdac4ccea4a28 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Wed, 22 Apr 2020 13:47:46 +0300 Subject: [PATCH 27/30] Ehance PlanDefinitionList snapshot --- .../tests/__snapshots__/index.test.tsx.snap | 185 ++++-------------- .../PlanDefinitionList/tests/index.test.tsx | 6 +- 2 files changed, 42 insertions(+), 149 deletions(-) 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 59b6e93e4a..5fe9263879 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,76 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: header bread crumb 1`] = ` - -
- - - -
-
+ "label": "Home", + "url": "/", + }, + ], +} `; -exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: list view data 1`] = ` +exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: list view props 1`] = ` Object { "data": Array [ Array [ @@ -217,85 +162,33 @@ exports[`components/InterventionPlan/PlanDefinitionList renders plan definition `; -exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: search form 1`] = ` - -
- - -
- - - -
-
- - -
- -
+ }, + "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 ee36a12c2a..e3d6f6ef9e 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -72,10 +72,10 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { ); - expect(toJson(wrapper.find('HeaderBreadcrumb'))).toMatchSnapshot('header bread crumb'); + expect(wrapper.find('HeaderBreadcrumb').props()).toMatchSnapshot('bread crumb props'); expect(toJson(wrapper.find('Row').at(0))).toMatchSnapshot('row heading'); - expect(toJson(wrapper.find('SearchForm'))).toMatchSnapshot('search form'); - expect(wrapper.find('ListView').props()).toMatchSnapshot('list view data'); + expect(wrapper.find('SearchForm').props()).toMatchSnapshot('search form props'); + expect(wrapper.find('ListView').props()).toMatchSnapshot('list view props'); wrapper.unmount(); }); From ce7f3787a1db62db837fdb99e062c57fc1825e17 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Wed, 22 Apr 2020 13:57:29 +0300 Subject: [PATCH 28/30] Add HelmetWrapper snapshot for PlanDefinitionList --- .../tests/__snapshots__/index.test.tsx.snap | 21 +++++++++++++++++++ .../PlanDefinitionList/tests/index.test.tsx | 1 + 2 files changed, 22 insertions(+) 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 5fe9263879..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 @@ -15,6 +15,27 @@ Object { } `; +exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: helmet 1`] = ` + + + + + +`; + exports[`components/InterventionPlan/PlanDefinitionList renders plan definition list correctly: list view props 1`] = ` Object { "data": Array [ diff --git a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx index e3d6f6ef9e..cac8a9be4a 100644 --- a/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx +++ b/src/containers/pages/InterventionPlan/PlanDefinitionList/tests/index.test.tsx @@ -76,6 +76,7 @@ describe('components/InterventionPlan/PlanDefinitionList', () => { 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(); }); From 75f0b582999ebf95ef5d584987b35fbb65e2ab64 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Wed, 22 Apr 2020 15:30:32 +0300 Subject: [PATCH 29/30] Make SearchForm prop placeholder not optional --- src/components/forms/Search/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/forms/Search/index.tsx b/src/components/forms/Search/index.tsx index d8765fa5d7..37882326ca 100644 --- a/src/components/forms/Search/index.tsx +++ b/src/components/forms/Search/index.tsx @@ -21,7 +21,7 @@ export type Submit = (event: React.FormEvent) => void; export interface SearchFormProps { history: History; location: Location; - placeholder?: string; + placeholder: string; } /** From 4de5360d0035e215b76df09c81e08073ad55fe05 Mon Sep 17 00:00:00 2001 From: Kelvin Muchiri Date: Wed, 22 Apr 2020 15:32:50 +0300 Subject: [PATCH 30/30] Remove repetition --- src/helpers/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/utils.tsx b/src/helpers/utils.tsx index 928954c705..1f6ed43951 100644 --- a/src/helpers/utils.tsx +++ b/src/helpers/utils.tsx @@ -876,7 +876,7 @@ export const reactSelectNoOptionsText = () => NO_OPTIONS; /** * Get query params from URL - * @param {Location} location location object from props + * @param {Location} location from props */ export const getQueryParams = (location: Location) => { return querystring.parse(trimStart(location.search, '?'));