Skip to content

Commit

Permalink
Merge pull request #80 from dhis2/move-list-state-to-store
Browse files Browse the repository at this point in the history
fix(list): move list state to store
  • Loading branch information
ismay authored Feb 23, 2021
2 parents 981f147 + 539ea77 commit 7238e15
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 177 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"devDependencies": {
"@dhis2/cli-app-scripts": "^4.0.9",
"@dhis2/cli-style": "^7.1.0-alpha.9",
"@testing-library/react-hooks": "^5.0.3",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.6",
"eslint-plugin-compat": "^3.9.0",
Expand Down
1 change: 1 addition & 0 deletions src/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ module.exports = {
files: ['*.test.js'],
rules: {
'i18next/no-literal-string': 'off',
'react/prop-types': 'off',
},
},
],
Expand Down
9 changes: 7 additions & 2 deletions src/components/Store/Store.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useState } from 'react'
import { PropTypes } from '@dhis2/prop-types'
import { CircularLoader, Layer, CenteredContent } from '@dhis2/ui'
import { useDataQuery } from '@dhis2/app-runtime'
Expand Down Expand Up @@ -62,6 +62,10 @@ const optionsQuery = {
}

const Store = ({ children }) => {
// State that should persist after a refetch
const jobFilterState = useState('')
const showSystemJobsState = useState(false)

const jobsFetch = useDataQuery(jobsQuery)
const jobTypesFetch = useDataQuery(jobTypesQuery)
const optionsFetch = useDataQuery(optionsQuery)
Expand Down Expand Up @@ -98,7 +102,6 @@ const Store = ({ children }) => {
predictors: { predictors },
predictorGroups: { predictorGroups },
} = optionsFetch.data

const parameterOptions = {
skipTableTypes,
validationRuleGroups,
Expand All @@ -114,6 +117,8 @@ const Store = ({ children }) => {
jobTypes,
parameterOptions,
refetchJobs: jobsFetch.refetch,
jobFilter: jobFilterState,
showSystemJobs: showSystemJobsState,
}}
>
{children}
Expand Down
48 changes: 48 additions & 0 deletions src/components/Store/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useContext } from 'react'
import StoreContext from './StoreContext'

export const useJobs = () => {
const store = useContext(StoreContext)
return store.jobs
}

/**
* This state is used in the job list, but kept in the
* store since it has to persist after a refetch.
*/
export const useJobFilter = () => {
const store = useContext(StoreContext)
return store.jobFilter
}

/**
* This state is used in the job list, but kept in the
* store since it has to persist after a refetch.
*/
export const useShowSystemJobs = () => {
const store = useContext(StoreContext)
return store.showSystemJobs
}

/**
* This hook returns the list of jobs that's shown in the
* job list route. The list is filtered by the job filter
* string and the show system jobs toggle from the store
* state.
*/
export const useListJobs = () => {
const [jobFilter] = useJobFilter()
const [showSystemJobs] = useShowSystemJobs()
const jobs = useJobs()

// Filter jobs by the current jobFilter
const applyJobFilter = job =>
job.name.toLowerCase().includes(jobFilter.toLowerCase())

// Filter jobs depending on the current showSystemJobs
const applyShowSystemJobs = job =>
// Jobs that are configurable are user jobs
showSystemJobs ? true : job.configurable

return jobs.filter(applyJobFilter).filter(applyShowSystemJobs)
}
140 changes: 140 additions & 0 deletions src/components/Store/hooks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React from 'react'
import { renderHook } from '@testing-library/react-hooks'
import { useJobFilter, useShowSystemJobs, useJobs, useListJobs } from './hooks'
import StoreContext from './StoreContext'

describe('useJobs', () => {
it('should return the jobs part of the store', () => {
const jobs = 'jobs'
const store = {
jobs,
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useJobs(), { wrapper })

expect(result.current).toBe(jobs)
})
})

describe('useJobFilter', () => {
it('should return the jobFilter part of the store', () => {
const jobFilter = 'jobFilter'
const store = {
jobFilter,
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useJobFilter(), { wrapper })

expect(result.current).toBe(jobFilter)
})
})

describe('useShowSystemJobs', () => {
it('should return the showSystemJobs part of the store', () => {
const showSystemJobs = 'showSystemJobs'
const store = {
showSystemJobs,
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useShowSystemJobs(), { wrapper })

expect(result.current).toBe(showSystemJobs)
})
})

describe('useListJobs', () => {
const user1 = {
name: 'User One',
configurable: true,
}
const user2 = {
name: 'User Two',
configurable: true,
}
const system1 = {
name: 'System One',
configurable: false,
}
const system2 = {
name: 'System Two',
configurable: false,
}

it('should return matching jobs when there is no filter and showSystemJobs is false', () => {
const store = {
jobFilter: [''],
showSystemJobs: [false],
jobs: [user1, user2, system1, system2],
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useListJobs(), { wrapper })

expect(result.current).toEqual(expect.arrayContaining([user1, user2]))
})

it('should return matching jobs when there is no filter and showSystemJobs is true', () => {
const store = {
jobFilter: [''],
showSystemJobs: [true],
jobs: [user1, user2, system1, system2],
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useListJobs(), { wrapper })

expect(result.current).toEqual(
expect.arrayContaining([user1, user2, system1, system2])
)
})

it('should return matching jobs when there is a filter and showSystemJobs is false', () => {
const store = {
jobFilter: ['One'],
showSystemJobs: [false],
jobs: [user1, user2, system1, system2],
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useListJobs(), { wrapper })

expect(result.current).toEqual(expect.arrayContaining([user1]))
})

it('should return matching jobs when there is a filter and showSystemJobs is true', () => {
const store = {
jobFilter: ['One'],
showSystemJobs: [true],
jobs: [user1, user2, system1, system2],
}
const wrapper = ({ children }) => (
<StoreContext.Provider value={store}>
{children}
</StoreContext.Provider>
)
const { result } = renderHook(() => useListJobs(), { wrapper })

expect(result.current).toEqual(expect.arrayContaining([user1, system1]))
})
})
3 changes: 2 additions & 1 deletion src/components/Store/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Store from './Store'
import StoreContext from './StoreContext'
import * as selectors from './selectors'
import * as hooks from './hooks'

export { Store, StoreContext, selectors }
export { Store, StoreContext, selectors, hooks }
18 changes: 5 additions & 13 deletions src/pages/JobList/JobListContainer.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import React, { useState, useContext } from 'react'
import { StoreContext, selectors } from '../../components/Store'
import React from 'react'
import { hooks } from '../../components/Store'
import JobList from './JobList'
import { getJobsMatchingFilter, getUserJobs } from './selectors'

const JobListContainer = () => {
const [showSystemJobs, setShowSystemJobs] = useState(false)
const [jobFilter, setJobFilter] = useState('')
const store = useContext(StoreContext)
const allJobs = selectors.getJobsStore(store)

// Filter jobs by the jobFilter string
const filteredJobs = getJobsMatchingFilter(allJobs, jobFilter)

// Show or hide system jobs
const jobs = showSystemJobs ? filteredJobs : getUserJobs(filteredJobs)
const [jobFilter, setJobFilter] = hooks.useJobFilter()
const [showSystemJobs, setShowSystemJobs] = hooks.useShowSystemJobs()
const jobs = hooks.useListJobs()

return (
<JobList
Expand Down
91 changes: 3 additions & 88 deletions src/pages/JobList/JobListContainer.test.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import React from 'react'
import { shallow, mount } from 'enzyme'
import { shallow } from 'enzyme'
import { StoreContext } from '../../components/Store'
import JobList from './JobList'
import JobListContainer from './JobListContainer'

jest.mock('./JobList', () => jest.fn())

afterEach(() => {
jest.resetAllMocks()
})

describe('<JobListContainer>', () => {
it('renders without errors when there is data', () => {
const store = {
jobs: [
{ id: 'one', name: 'one' },
{ id: 'two', name: 'two' },
],
jobFilter: ['', () => {}],
showSystemJobs: [false, () => {}],
}

shallow(
Expand All @@ -25,84 +20,4 @@ describe('<JobListContainer>', () => {
</StoreContext.Provider>
)
})

it('omits system jobs by default', () => {
JobList.mockImplementation(() => null)

const userJob = { id: 'user', name: 'user', configurable: true }
const systemJob = { id: 'system', name: 'system' }
const store = {
jobs: [userJob, systemJob],
}

const wrapper = mount(
<StoreContext.Provider value={store}>
<JobListContainer />
</StoreContext.Provider>
)
const childProps = wrapper.children().props()

expect(childProps.jobs).toHaveLength(1)
expect(childProps.jobs).toEqual(expect.arrayContaining([userJob]))
})

it('passes system and user jobs after toggling', () => {
JobList.mockImplementation(({ showSystemJobs, setShowSystemJobs }) => (
<button
data-test="mock-toggle"
onClick={() => setShowSystemJobs(!showSystemJobs)}
/>
))

const userJob = { id: 'user', name: 'user', configurable: true }
const systemJob = { id: 'system', name: 'system' }
const store = {
jobs: [userJob, systemJob],
}

const wrapper = mount(
<StoreContext.Provider value={store}>
<JobListContainer />
</StoreContext.Provider>
)

wrapper.find({ 'data-test': 'mock-toggle' }).simulate('click')

const childProps = wrapper.children().props()

expect(childProps.jobs).toHaveLength(2)
expect(childProps.jobs).toEqual(
expect.arrayContaining([userJob, systemJob])
)
})

it('filters jobs after updating the filter', () => {
JobList.mockImplementation(({ setJobFilter }) => (
<input
data-test="mock-input"
onChange={e => setJobFilter(e.target.value)}
/>
))

const one = { id: 'one', name: 'one', configurable: true }
const two = { id: 'two', name: 'two', configurable: true }
const store = {
jobs: [one, two],
}

const wrapper = mount(
<StoreContext.Provider value={store}>
<JobListContainer />
</StoreContext.Provider>
)

wrapper
.find({ 'data-test': 'mock-input' })
.simulate('change', { target: { value: 'two' } })

const childProps = wrapper.children().props()

expect(childProps.jobs).toHaveLength(1)
expect(childProps.jobs).toEqual(expect.arrayContaining([two]))
})
})
6 changes: 0 additions & 6 deletions src/pages/JobList/selectors.js

This file was deleted.

Loading

0 comments on commit 7238e15

Please sign in to comment.