Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
feat(incidents): filter incidents (#2087)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcosvega91 committed May 20, 2020
1 parent d1fb940 commit 5309a85
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 32 deletions.
17 changes: 15 additions & 2 deletions src/__tests__/incidents/incidents-slice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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))
})
})
})
})
22 changes: 22 additions & 0 deletions src/__tests__/incidents/list/ViewIncidents.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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])
Expand Down
18 changes: 18 additions & 0 deletions src/clients/db/IncidentRepository.ts
Original file line number Diff line number Diff line change
@@ -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<Incident> {
constructor() {
super(incidents)
}

async search(options: SearchOptions): Promise<Incident[]> {
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()
6 changes: 6 additions & 0 deletions src/incidents/IncidentFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum IncidentFilter {
reported = 'reported',
all = 'all',
}

export default IncidentFilter
13 changes: 10 additions & 3 deletions src/incidents/incidents-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -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))
}
Expand Down
81 changes: 54 additions & 27 deletions src/incidents/list/ViewIncidents.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLSelectElement>) => {
setSearchFilter(event.target.value as IncidentFilter)
}

const filterOptions = Object.values(IncidentFilter).map((filter) => ({
label: t(`incidents.status.${filter}`),
value: `${filter}`,
}))

return (
<table className="table table-hover">
<thead className="thead-light">
<tr>
<th>{t('incidents.reports.code')}</th>
<th>{t('incidents.reports.dateOfIncident')}</th>
<th>{t('incidents.reports.reportedBy')}</th>
<th>{t('incidents.reports.reportedOn')}</th>
<th>{t('incidents.reports.status')}</th>
</tr>
</thead>
<tbody>
{incidents.map((incident: Incident) => (
<tr onClick={() => onTableRowClick(incident)} key={incident.id}>
<td>{incident.code}</td>
<td>{format(new Date(incident.date), 'yyyy-MM-dd hh:mm a')}</td>
<td>{incident.reportedBy}</td>
<td>{format(new Date(incident.reportedOn), 'yyyy-MM-dd hh:mm a')}</td>
<td>{incident.status}</td>
</tr>
))}
</tbody>
</table>
<>
<div className="row">
<div className="col-md-3 col-lg-2">
<SelectWithLabelFormGroup
name="type"
value={searchFilter}
label={t('incidents.filterTitle')}
isEditable
options={filterOptions}
onChange={onFilterChange}
/>
</div>
</div>
<div className="row">
<table className="table table-hover">
<thead className="thead-light">
<tr>
<th>{t('incidents.reports.code')}</th>
<th>{t('incidents.reports.dateOfIncident')}</th>
<th>{t('incidents.reports.reportedBy')}</th>
<th>{t('incidents.reports.reportedOn')}</th>
<th>{t('incidents.reports.status')}</th>
</tr>
</thead>
<tbody>
{incidents.map((incident: Incident) => (
<tr onClick={() => onTableRowClick(incident)} key={incident.id}>
<td>{incident.code}</td>
<td>{format(new Date(incident.date), 'yyyy-MM-dd hh:mm a')}</td>
<td>{incident.reportedBy}</td>
<td>{format(new Date(incident.reportedOn), 'yyyy-MM-dd hh:mm a')}</td>
<td>{incident.status}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)
}

Expand Down
5 changes: 5 additions & 0 deletions src/locales/enUs/translations/incidents/index.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down

1 comment on commit 5309a85

@vercel
Copy link

@vercel vercel bot commented on 5309a85 May 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.