From 5309a859a4b19617f44183b1d70d0f36af6206d8 Mon Sep 17 00:00:00 2001 From: Marco Moretti Date: Wed, 20 May 2020 02:41:01 +0200 Subject: [PATCH] feat(incidents): filter incidents (#2087) --- .../incidents/incidents-slice.test.ts | 17 +++- .../incidents/list/ViewIncidents.test.tsx | 22 +++++ src/clients/db/IncidentRepository.ts | 18 +++++ src/incidents/IncidentFilter.ts | 6 ++ src/incidents/incidents-slice.ts | 13 ++- src/incidents/list/ViewIncidents.tsx | 81 ++++++++++++------- .../enUs/translations/incidents/index.ts | 5 ++ 7 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 src/incidents/IncidentFilter.ts diff --git a/src/__tests__/incidents/incidents-slice.test.ts b/src/__tests__/incidents/incidents-slice.test.ts index 6dbddd628f..47dd1b4656 100644 --- a/src/__tests__/incidents/incidents-slice.test.ts +++ b/src/__tests__/incidents/incidents-slice.test.ts @@ -3,10 +3,11 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import IncidentRepository from '../../clients/db/IncidentRepository' +import IncidentFilter from '../../incidents/IncidentFilter' import incidents, { - fetchIncidents, fetchIncidentsStart, fetchIncidentsSuccess, + searchIncidents, } from '../../incidents/incidents-slice' import Incident from '../../model/Incident' import { RootState } from '../../store' @@ -39,12 +40,24 @@ describe('Incidents Slice', () => { jest.spyOn(IncidentRepository, 'findAll').mockResolvedValue(expectedIncidents) const store = mockStore() - await store.dispatch(fetchIncidents()) + await store.dispatch(searchIncidents(IncidentFilter.all)) expect(store.getActions()[0]).toEqual(fetchIncidentsStart()) expect(IncidentRepository.findAll).toHaveBeenCalledTimes(1) expect(store.getActions()[1]).toEqual(fetchIncidentsSuccess(expectedIncidents)) }) + + it('should fetch incidents filtering by status', async () => { + const expectedIncidents = [{ id: '123' }] as Incident[] + jest.spyOn(IncidentRepository, 'search').mockResolvedValue(expectedIncidents) + const store = mockStore() + + await store.dispatch(searchIncidents(IncidentFilter.reported)) + + expect(store.getActions()[0]).toEqual(fetchIncidentsStart()) + expect(IncidentRepository.search).toHaveBeenCalledTimes(1) + expect(store.getActions()[1]).toEqual(fetchIncidentsSuccess(expectedIncidents)) + }) }) }) }) diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx index 13b2fcbe9a..ab3dff4db2 100644 --- a/src/__tests__/incidents/list/ViewIncidents.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx @@ -11,6 +11,7 @@ import thunk from 'redux-thunk' import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' import IncidentRepository from '../../../clients/db/IncidentRepository' +import IncidentFilter from '../../../incidents/IncidentFilter' import ViewIncidents from '../../../incidents/list/ViewIncidents' import Incident from '../../../model/Incident' import Permissions from '../../../model/Permissions' @@ -42,6 +43,7 @@ describe('View Incidents', () => { jest.spyOn(titleUtil, 'default') jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(IncidentRepository, 'findAll').mockResolvedValue(expectedIncidents) + jest.spyOn(IncidentRepository, 'search').mockResolvedValue(expectedIncidents) history = createMemoryHistory() history.push(`/incidents`) @@ -72,7 +74,27 @@ describe('View Incidents', () => { wrapper.update() return wrapper } + it('should filter incidents by status=reported on first load ', async () => { + const wrapper = await setup([Permissions.ViewIncidents]) + const filterSelect = wrapper.find('select') + expect(filterSelect.props().value).toBe(IncidentFilter.reported) + expect(IncidentRepository.search).toHaveBeenCalled() + expect(IncidentRepository.search).toHaveBeenCalledWith({ status: IncidentFilter.reported }) + }) + it('should call IncidentRepository after changing filter', async () => { + const wrapper = await setup([Permissions.ViewIncidents]) + const filterSelect = wrapper.find('select') + + expect(IncidentRepository.findAll).not.toHaveBeenCalled() + + filterSelect.simulate('change', { target: { value: IncidentFilter.all } }) + expect(IncidentRepository.findAll).toHaveBeenCalled() + filterSelect.simulate('change', { target: { value: IncidentFilter.reported } }) + + expect(IncidentRepository.search).toHaveBeenCalledTimes(2) + expect(IncidentRepository.search).toHaveBeenLastCalledWith({ status: IncidentFilter.reported }) + }) describe('layout', () => { it('should set the title', async () => { await setup([Permissions.ViewIncidents]) diff --git a/src/clients/db/IncidentRepository.ts b/src/clients/db/IncidentRepository.ts index 204309a0c2..dc6fbb7382 100644 --- a/src/clients/db/IncidentRepository.ts +++ b/src/clients/db/IncidentRepository.ts @@ -1,11 +1,29 @@ import { incidents } from '../../config/pouchdb' +import IncidentFilter from '../../incidents/IncidentFilter' import Incident from '../../model/Incident' import Repository from './Repository' +interface SearchOptions { + status: IncidentFilter +} class IncidentRepository extends Repository { constructor() { super(incidents) } + + async search(options: SearchOptions): Promise { + return super.search(IncidentRepository.getSearchCriteria(options)) + } + + private static getSearchCriteria(options: SearchOptions): any { + const statusFilter = options.status !== IncidentFilter.all ? [{ status: options.status }] : [] + const selector = { + $and: statusFilter, + } + return { + selector, + } + } } export default new IncidentRepository() diff --git a/src/incidents/IncidentFilter.ts b/src/incidents/IncidentFilter.ts new file mode 100644 index 0000000000..d141730d05 --- /dev/null +++ b/src/incidents/IncidentFilter.ts @@ -0,0 +1,6 @@ +enum IncidentFilter { + reported = 'reported', + all = 'all', +} + +export default IncidentFilter diff --git a/src/incidents/incidents-slice.ts b/src/incidents/incidents-slice.ts index b83bef38a2..1d35600312 100644 --- a/src/incidents/incidents-slice.ts +++ b/src/incidents/incidents-slice.ts @@ -3,6 +3,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import IncidentRepository from '../clients/db/IncidentRepository' import Incident from '../model/Incident' import { AppThunk } from '../store' +import IncidentFilter from './IncidentFilter' interface IncidentsState { incidents: Incident[] @@ -34,10 +35,16 @@ const incidentSlice = createSlice({ export const { fetchIncidentsStart, fetchIncidentsSuccess } = incidentSlice.actions -export const fetchIncidents = (): AppThunk => async (dispatch) => { +export const searchIncidents = (status: IncidentFilter): AppThunk => async (dispatch) => { dispatch(fetchIncidentsStart()) - - const incidents = await IncidentRepository.findAll() + let incidents + if (status === IncidentFilter.all) { + incidents = await IncidentRepository.findAll() + } else { + incidents = await IncidentRepository.search({ + status, + }) + } dispatch(fetchIncidentsSuccess(incidents)) } diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx index 4c97158ba8..473ed240ce 100644 --- a/src/incidents/list/ViewIncidents.tsx +++ b/src/incidents/list/ViewIncidents.tsx @@ -1,53 +1,80 @@ import format from 'date-fns/format' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' +import SelectWithLabelFormGroup from '../../components/input/SelectWithLableFormGroup' import Incident from '../../model/Incident' import useTitle from '../../page-header/useTitle' import { RootState } from '../../store' -import { fetchIncidents } from '../incidents-slice' +import IncidentFilter from '../IncidentFilter' +import { searchIncidents } from '../incidents-slice' const ViewIncidents = () => { const { t } = useTranslation() const history = useHistory() const dispatch = useDispatch() useTitle(t('incidents.reports.label')) - + const [searchFilter, setSearchFilter] = useState(IncidentFilter.reported) const { incidents } = useSelector((state: RootState) => state.incidents) useEffect(() => { - dispatch(fetchIncidents()) - }, [dispatch]) + dispatch(searchIncidents(searchFilter)) + }, [dispatch, searchFilter]) const onTableRowClick = (incident: Incident) => { history.push(`incidents/${incident.id}`) } + const onFilterChange = (event: React.ChangeEvent) => { + setSearchFilter(event.target.value as IncidentFilter) + } + + const filterOptions = Object.values(IncidentFilter).map((filter) => ({ + label: t(`incidents.status.${filter}`), + value: `${filter}`, + })) + return ( - - - - - - - - - - - - {incidents.map((incident: Incident) => ( - onTableRowClick(incident)} key={incident.id}> - - - - - - - ))} - -
{t('incidents.reports.code')}{t('incidents.reports.dateOfIncident')}{t('incidents.reports.reportedBy')}{t('incidents.reports.reportedOn')}{t('incidents.reports.status')}
{incident.code}{format(new Date(incident.date), 'yyyy-MM-dd hh:mm a')}{incident.reportedBy}{format(new Date(incident.reportedOn), 'yyyy-MM-dd hh:mm a')}{incident.status}
+ <> +
+
+ +
+
+
+ + + + + + + + + + + + {incidents.map((incident: Incident) => ( + onTableRowClick(incident)} key={incident.id}> + + + + + + + ))} + +
{t('incidents.reports.code')}{t('incidents.reports.dateOfIncident')}{t('incidents.reports.reportedBy')}{t('incidents.reports.reportedOn')}{t('incidents.reports.status')}
{incident.code}{format(new Date(incident.date), 'yyyy-MM-dd hh:mm a')}{incident.reportedBy}{format(new Date(incident.reportedOn), 'yyyy-MM-dd hh:mm a')}{incident.status}
+
+ ) } diff --git a/src/locales/enUs/translations/incidents/index.ts b/src/locales/enUs/translations/incidents/index.ts index d6cadc5994..8b71520a52 100644 --- a/src/locales/enUs/translations/incidents/index.ts +++ b/src/locales/enUs/translations/incidents/index.ts @@ -1,9 +1,14 @@ export default { incidents: { + filterTitle: ' Filter by status', label: 'Incidents', actions: { report: 'Report', }, + status: { + reported: 'reported', + all: 'all', + }, reports: { label: 'Reported Incidents', new: 'Report Incident',