Skip to content

Commit

Permalink
Update the chart filter form
Browse files Browse the repository at this point in the history
This commit:

* Renames the ChartFilterForm (it was misspelled previously)
* Adds a test for the ChartFiltersForm
* Adds a new ControlledMultiSelect input using MUI components
* Updates the ChartFiltersForm and ViewChartPage to use the new form conventions
  • Loading branch information
mikestone14 committed Nov 9, 2023
1 parent ba427d0 commit 9d5dc24
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 37 deletions.
143 changes: 143 additions & 0 deletions views/forms/ChartFilterForm/ChartFilterForm.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React from 'react'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import ChartFilterForm from '.'

describe('Chart Filter Form', () => {
const defaultProps = {
initialValues: {
chrcrit_part: [
{ name: 'HC', value: 'false' },
{ name: 'CHR', value: 'false' },
{ name: 'Missing', value: 'false' },
],
included_excluded: [
{ name: 'Included', value: 'false' },
{ name: 'Excluded', value: 'false' },
{ name: 'Missing', value: 'false' },
],
sex_at_birth: [
{ name: 'Male', value: 'false' },
{ name: 'Female', value: 'false' },
{ name: 'Missing', value: 'false' },
],
sites: ['CA', 'LA'],
},
siteOptions: [
{ label: 'CA', value: 'CA' },
{ label: 'LA', value: 'LA' },
{ label: 'MA', value: 'MA' },
],
onSubmit: () => {},
}
const elements = {
hcField: () =>
screen.getByRole('checkbox', { name: 'chrcrit_part.0.value' }),
chrField: () =>
screen.getByRole('checkbox', { name: 'chrcrit_part.1.value' }),
includedExcludedMissing: () =>
screen.getByRole('checkbox', { name: 'included_excluded.2.value' }),
siteSelect: () => screen.getByLabelText('Sites'),
siteOptions: () => screen.getByRole('listbox'),
submitBtn: () => screen.getByText('Apply Filters'),
}
const renderForm = (props = defaultProps) => {
render(<ChartFilterForm {...props} />)
}

test('calls the onSubmit prop when the form is submitted with valid data', async () => {
const user = userEvent.setup()
const onSubmit = jest.fn()
const props = { ...defaultProps, onSubmit }

renderForm(props)
await user.click(elements.hcField())
await user.click(elements.includedExcludedMissing())
await user.click(elements.siteSelect())
await user.click(within(elements.siteOptions()).getByText('CA'))
await user.click(within(elements.siteOptions()).getByText('MA'))
await user.click(elements.submitBtn())

await waitFor(() =>
expect(onSubmit).toHaveBeenCalledWith(
{
chrcrit_part: [
{ name: 'HC', value: true },
{ name: 'CHR', value: 'false' },
{ name: 'Missing', value: 'false' },
],
included_excluded: [
{ name: 'Included', value: 'false' },
{ name: 'Excluded', value: 'false' },
{ name: 'Missing', value: true },
],
sex_at_birth: [
{ name: 'Male', value: 'false' },
{ name: 'Female', value: 'false' },
{ name: 'Missing', value: 'false' },
],
sites: ['LA', 'MA'],
},
expect.anything()
)
)
})

// test('displays errors and does not submit the form with invalid data', async () => {
// const user = userEvent.setup()
// const onSubmit = jest.fn()
// const props = { ...defaultProps, onSubmit }

// renderForm(props)
// await user.type(elements.usernameField(), 'myUsername')
// await user.type(elements.passwordField(), 'nope')
// await user.type(elements.confirmPasswordField(), 'nope')
// await user.type(elements.fullNameField(), 'Joe Schmoe')
// await user.type(elements.emailField(), 'not-an-email')
// await user.click(elements.submitBtn())

// await waitFor(() =>
// expect(
// screen.getByText('password must be at least 8 characters')
// ).toBeInTheDocument()
// )
// expect(screen.getByText('email must be a valid email')).toBeInTheDocument()

// expect(onSubmit).not.toHaveBeenCalled()
// })

// test('displays errors and does not submit the form with unmatched passwords', async () => {
// const user = userEvent.setup()
// const onSubmit = jest.fn()
// const props = { ...defaultProps, onSubmit }

// renderForm(props)
// await user.type(elements.usernameField(), 'myUsername')
// await user.type(elements.passwordField(), 'thisIsMyPassword')
// await user.type(
// elements.confirmPasswordField(),
// 'thisIsMyConfirmedPassword'
// )
// await user.type(elements.fullNameField(), 'Joe Schmoe')
// await user.type(elements.emailField(), 'joe@example.test')
// await user.click(elements.submitBtn())

// await waitFor(() =>
// expect(screen.getByText('passwords do not match')).toBeInTheDocument()
// )

// expect(onSubmit).not.toHaveBeenCalled()
// })

// test('calls the onCancel prop when the cancel button is clicked', async () => {
// const user = userEvent.setup()
// const onCancel = jest.fn()
// const props = { ...defaultProps, onCancel }

// renderForm(props)
// await user.click(elements.cancelBtn())

// await waitFor(() => expect(onCancel).toHaveBeenCalledWith())
// })
})
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import React from 'react'
import { useForm } from 'react-hook-form'
import { Button, List, ListItem, Typography, InputLabel } from '@mui/material'

import { FILTER_CATEGORIES, TRUE_STRING } from '../../../constants'
import ControlledCheckbox from '../ControlledCheckbox'
import ControlledReactSelect from '../ControlledReactSelect'
import ControlledMultiSelect from '../ControlledMultiSelect'

const ChartFilterForm = ({
initialValues,
onSubmit,
const ChartFilterForm = ({ initialValues, onSubmit, siteOptions }) => {
const { handleSubmit, control } = useForm({
defaultValues: initialValues,
})

control,
siteOptions,
}) => {
return (
<form onSubmit={onSubmit}>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
{Object.keys(initialValues)
.filter((key) => key !== 'sites')
Expand Down Expand Up @@ -49,13 +48,16 @@ const ChartFilterForm = ({
})}
</div>
<div>
<Typography variant="subtitle2">Sites</Typography>
<ControlledReactSelect
<InputLabel id="sites-select" variant="subtitle2">
Sites
</InputLabel>
<ControlledMultiSelect
labelId="sites-select"
name="sites"
control={control}
options={siteOptions}
placeholder="Select a site to view data"
isMulti
fullWidth
/>
</div>
<div>
Expand Down
9 changes: 4 additions & 5 deletions views/forms/ControlledCheckbox/index.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React from 'react'
import { Controller } from 'react-hook-form'
import { Checkbox } from '@mui/material'

const ControlledCheckbox = ({ name, control, onChange, checked }) => {
const ControlledCheckbox = ({ name, control, checked }) => {
return (
<Controller
name={name}
control={control}
render={({ field }) => (
<Checkbox
onChange={(e) => {
field.onChange(e)
if (onChange) onChange()
}}
{...field}
inputProps={{ name, 'aria-label': name }}
defaultChecked={checked}
/>
)}
Expand Down
35 changes: 35 additions & 0 deletions views/forms/ControlledMultiSelect/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react'
import { Controller } from 'react-hook-form'
import Select from '@mui/material/Select'
import { Box, Chip, MenuItem } from '@mui/material'

const ControlledMultiSelect = ({ name, control, options, ...rest }) => {
return (
<Controller
name={name}
control={control}
render={({ field }) => (
<Select
{...rest}
{...field}
multiple
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{options.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
)}
/>
)
}

export default ControlledMultiSelect
29 changes: 8 additions & 21 deletions views/pages/ViewChartPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,30 @@ import {
useLocation,
useNavigate,
} from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { Typography } from '@mui/material'

import BarGraph from '../components/BarGraph'
import GraphTable from '../components/GraphTable'
import UserAvatar from '../components/UserAvatar'
import ChartFilterForm from '../forms/CharFilterForm'
import ChartFilterForm from '../forms/ChartFilterForm'
import { apiRoutes, routes } from '../routes/routes'
import api from '../api'
import StudiesModel from '../models/StudiesModel'

const ViewChartPage = () => {
const { setNotification } = useOutletContext()
const { setNotification } = useOutletContext()
const { search } = useLocation()
const { chart_id } = useParams()
const navigate = useNavigate()
const [graph, setGraph] = useState(null)
const { handleSubmit, control, reset } = useForm()
const handleFormSubmit = async (updatedFilters) => {
if (!updatedFilters.sites.length) {
const onSubmit = async (filters) => {
if (!filters.sites.length) {
setNotification({
open: true,
message: 'Please select a site to view data',
})
} else {
const sites = updatedFilters.sites.map((obj) => obj.value)
const queryParams = {
filters: { ...updatedFilters, sites },
}
const newRoute = routes.viewChart(chart_id, queryParams)
const newRoute = routes.viewChart(chart_id, { filters })

navigate(newRoute)
}
Expand Down Expand Up @@ -64,10 +59,6 @@ const ViewChartPage = () => {

fetchGraph(chart_id, parsedQuery.filters).then((newGraph) => {
setGraph(newGraph)
reset({
...newGraph.filters,
sites: StudiesModel.dropdownSelectOptions(newGraph.filters.sites),
})
})
}, [chart_id, search])

Expand All @@ -87,13 +78,9 @@ const ViewChartPage = () => {
)}
<div>
<ChartFilterForm
initialValues={{
...graph.filters,
sites: StudiesModel.dropdownSelectOptions(graph.filters.sites),
}}
onSubmit={handleSubmit(handleFormSubmit)}
initialValues={graph.filters}
onSubmit={onSubmit}
siteOptions={StudiesModel.dropdownSelectOptions(graph.userSites)}
control={control}
/>
</div>
<BarGraph graph={graph} />
Expand Down

0 comments on commit 9d5dc24

Please sign in to comment.