Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e8b5f2b
WEB-4654 initial scaffolding for ActiveFiltersBar and AppliedFilters
henry-tp Jul 9, 2026
2680f37
WEB-4654 rename items to chips
henry-tp Jul 9, 2026
b54fc49
WEB-4654 refactor to get rid of one-off functions
henry-tp Jul 9, 2026
056fe0a
WEB-4654 update chip styling
henry-tp Jul 9, 2026
f583d39
WEB-4654 remove FilterResetBar and replace
henry-tp Jul 9, 2026
e4a29c1
WEB-4654 fix visual styling for applied filters
henry-tp Jul 9, 2026
b18c1f4
WEB-4654 fix Summarizing Period label and positioning
henry-tp Jul 9, 2026
8706f0b
WEB-4654 simplify Data Recency button
henry-tp Jul 9, 2026
ef612ae
WEB-4654 change order of filters in Pop Health
henry-tp Jul 9, 2026
e52b5af
WEB-4654 AppliedFilters takes search argument
henry-tp Jul 10, 2026
4ce23e7
WEB-4654 simplify chipgroup prefix
henry-tp Jul 10, 2026
9b2b4f4
WEB-4654 rename dropdown to Clinic Sites
henry-tp Jul 10, 2026
39f59b2
WEB-4654 AppliedFilters accepts rightContent
henry-tp Jul 13, 2026
cb8b216
WEB-4654 fix font sizing & spacing
henry-tp Jul 13, 2026
690abf4
WEB-4654 remove default case from AppliedFiltersAdapter
henry-tp Jul 13, 2026
8d3f1ec
WEB-4654 revert ordering
henry-tp Jul 14, 2026
7b8f0f5
WEB-4654 abstract ClearFilterButtons
henry-tp Jul 14, 2026
0c49625
WEB-4654 fix prop name bug
henry-tp Jul 14, 2026
bafce2c
Merge branch 'feat/filter-enhancements' into WEB-4654-filter-bar
henry-tp Jul 14, 2026
71310fd
WEB-4654 fix missing print fn
henry-tp Jul 14, 2026
46fdaae
WEB-4654 revert changes to SummaryPeriod filter
henry-tp Jul 15, 2026
c576f4e
WEB-4654 revert changes to SummaryPeriod filter
henry-tp Jul 15, 2026
68d4eb0
WEB-4654 rename to ActiveFiltersTray
henry-tp Jul 15, 2026
b932de2
WEB-4654 re-add comments for `SPECIAL_FILTER_STATES`
henry-tp Jul 15, 2026
84a9a30
WEB-4654 change directory for AppliedFiltersList
henry-tp Jul 15, 2026
14c5cbd
WEB-4654 put tray-hiding behaviour into adapter component instead of …
henry-tp Jul 15, 2026
14195bb
WEB-4654 remove unused tests
henry-tp Jul 15, 2026
40980f8
WEB-4654 move ClearFilterButtons rendering into AppliedFiltersList
henry-tp Jul 15, 2026
65c2d02
WEB-4654 write tests for AppliedFiltersList
henry-tp Jul 15, 2026
d4930a8
WEB-4654 write tests for ActiveFilterTray
henry-tp Jul 15, 2026
bf7d7ac
WEB-4654 update copy text for no tag / no site in ActiveFiltersTray
henry-tp Jul 16, 2026
df238e9
WEB-4654 add correct symbol for cgm us
henry-tp Jul 17, 2026
747ee5b
WEB-4654 rename reset filters button
henry-tp Jul 17, 2026
47b21f5
WEB-4654 clean up logic for label determination
henry-tp Jul 17, 2026
ff3be8d
WEB-4654 fix breaking test
henry-tp Jul 17, 2026
dac1fd5
WEB-4654 remove unused var
henry-tp Jul 20, 2026
23c9bc2
WEB-4654 add missing pluralization strings
henry-tp Jul 20, 2026
a150632
WEb-4654 sort tags/sites alphabetically in tray
henry-tp Jul 21, 2026
9e6056b
WEB-4654 remove unnecessary comments in tests
henry-tp Jul 21, 2026
30d791a
WEB-4654 fix missing CGM use clear case
henry-tp Jul 26, 2026
7ac0870
WEB-4654 update imports for consistency
henry-tp Jul 26, 2026
b5f2665
WEB-4654 fix render condition
henry-tp Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'theme-ui';

import theme from '@app/themes/baseTheme';
import AppliedFiltersList from '@app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList';
import { defaultFilterState, SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';

const mockStore = configureStore([thunk]);

const FULLY_ACTIVE_FILTERS = {
...defaultFilterState,
lastData: 14,
lastDataType: 'cgm',
timeInRange: ['timeInTargetPercent', 'timeInVeryLowPercent'],
patientTags: ['tag1', 'tag2'],
clinicSites: ['site1', 'site2'],
};

const buildState = ({
fetchedPatientCount = 5,
patientListSearchTextInput = '',
} = {}) => ({
blip: {
selectedClinicId: 'clinic123',
clinics: {
'clinic123': {
id: 'clinic123',
fetchedPatientCount,
patientTags: [
{ id: 'tag1', name: 'Tag One' },
{ id: 'tag2', name: 'Tag Two' },
],
sites: [
{ id: 'site1', name: 'Site Alpha' },
{ id: 'site2', name: 'Site Bravo' },
],
},
},
patientListFilters: { patientListSearchTextInput },
},
});

const renderList = ({
activeFilters = defaultFilterState,
setActiveFilters = jest.fn(),
onClearSearch = jest.fn(),
onResetFilters = jest.fn(),
state = buildState(),
} = {}) => {
const store = mockStore(state);

const utils = render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<AppliedFiltersList
activeFilters={activeFilters}
setActiveFilters={setActiveFilters}
onClearSearch={onClearSearch}
onResetFilters={onResetFilters}
/>
</ThemeProvider>
</Provider>
);

return { ...utils, setActiveFilters, onClearSearch, onResetFilters };
};

describe('AppliedFiltersList', () => {
describe('clear/reset controls', () => {
it('shows a "Reset Filters" control that fires onResetFilters when only filters are active', async () => {
const { onResetFilters, onClearSearch } = renderList({
activeFilters: { ...defaultFilterState, timeInRange: ['timeInTargetPercent'] },
});

await userEvent.click(screen.getByRole('button', { name: 'Reset All Filters' }));

expect(onResetFilters).toHaveBeenCalledTimes(1);
expect(onClearSearch).not.toHaveBeenCalled();
});

it('shows a "Clear Search" control that fires onClearSearch when only a search is active', async () => {
const { onClearSearch, onResetFilters } = renderList({
activeFilters: defaultFilterState,
state: buildState({ patientListSearchTextInput: 'john' }),
});

await userEvent.click(screen.getByRole('button', { name: 'Clear Search' }));

expect(onClearSearch).toHaveBeenCalledTimes(1);
expect(onResetFilters).not.toHaveBeenCalled();
});

it('shows both controls, each wired to its own callback, when a filter and a search are both active', async () => {
const { onClearSearch, onResetFilters } = renderList({
activeFilters: { ...defaultFilterState, timeInRange: ['timeInTargetPercent'] },
state: buildState({ patientListSearchTextInput: 'john' }),
});

await userEvent.click(screen.getByRole('button', { name: 'Reset All Filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Clear Search' }));

expect(onResetFilters).toHaveBeenCalledTimes(1);
expect(onClearSearch).toHaveBeenCalledTimes(1);
});
});

describe('removing filters fires setActiveFilters correctly', () => {
it('resets lastData and lastDataType to their defaults when the data-recency chip is removed', async () => {
const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });

await userEvent.click(screen.getByLabelText('Remove CGM data within 14 days filter'));

expect(setActiveFilters).toHaveBeenCalledTimes(1);
expect(setActiveFilters).toHaveBeenCalledWith({
...FULLY_ACTIVE_FILTERS,
lastData: defaultFilterState.lastData,
lastDataType: defaultFilterState.lastDataType,
});
});

it('removes only the clicked time-in-range value, preserving the others', async () => {
const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });

await userEvent.click(screen.getByLabelText('Remove %TIR = Meeting Targets filter'));

expect(setActiveFilters).toHaveBeenCalledTimes(1);
expect(setActiveFilters).toHaveBeenCalledWith({
...FULLY_ACTIVE_FILTERS,
timeInRange: ['timeInVeryLowPercent'],
});
});

it('removes only the clicked patient tag, preserving the others', async () => {
const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });

await userEvent.click(screen.getByLabelText('Remove Tag One filter'));

expect(setActiveFilters).toHaveBeenCalledTimes(1);
expect(setActiveFilters).toHaveBeenCalledWith({
...FULLY_ACTIVE_FILTERS,
patientTags: ['tag2'],
});
});

it('removes only the clicked clinic site, preserving the others', async () => {
const { setActiveFilters } = renderList({ activeFilters: FULLY_ACTIVE_FILTERS });

await userEvent.click(screen.getByLabelText('Remove Site Alpha filter'));

expect(setActiveFilters).toHaveBeenCalledTimes(1);
expect(setActiveFilters).toHaveBeenCalledWith({
...FULLY_ACTIVE_FILTERS,
clinicSites: ['site2'],
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { Provider } from 'react-redux';
import { ThemeProvider } from 'theme-ui';
import '@app/core/language';

import theme from '@app/themes/baseTheme';
import ActiveFiltersTray from '@app/pages/clinicworkspace/components/ActiveFiltersTray';
import { defaultFilterState, SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';

const mockStore = configureStore([thunk]);

const buildState = ({ fetchedPatientCount = 5 } = {}) => ({
blip: {
selectedClinicId: 'clinic123',
clinics: {
'clinic123': {
id: 'clinic123',
fetchedPatientCount,
patientTags: [
{ id: 'tag1', name: 'Tag One' },
{ id: 'tag2', name: 'Tag Two' },
],
sites: [
{ id: 'site1', name: 'Site Alpha' },
{ id: 'site2', name: 'Site Bravo' },
],
},
},
},
});

const renderTray = ({
filters = defaultFilterState,
hasSearchActive = false,
onRemoveFilter = jest.fn(),
state = buildState(),
} = {}) => {
const store = mockStore(state);

const utils = render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<ActiveFiltersTray
filters={filters}
hasSearchActive={hasSearchActive}
onRemoveFilter={onRemoveFilter}
/>
</ThemeProvider>
</Provider>
);

return { ...utils, onRemoveFilter };
};

describe('ActiveFiltersTray', () => {
describe('patient count header', () => {
it('renders the fetched patient count', () => {
renderTray({ state: buildState({ fetchedPatientCount: 5 }) });

expect(screen.getByText('Showing 5 patients')).toBeInTheDocument();
});

it('notes the count reflects the search when a search is active', () => {
renderTray({ hasSearchActive: true, state: buildState({ fetchedPatientCount: 5 }) });

expect(screen.getByText('Showing 5 patients that match your search')).toBeInTheDocument();
});
});

describe('primary filter chips', () => {
it('renders a time-in-range filter under the "with" prefix using its expected label', () => {
renderTray({ filters: { ...defaultFilterState, timeInRange: ['timeInTargetPercent'] } });

expect(screen.getByText('with')).toBeInTheDocument();
expect(screen.getByText('%TIR = Meeting Targets')).toBeInTheDocument();
});

it('renders a data-recency filter with its expected label', () => {
renderTray({ filters: { ...defaultFilterState, lastData: 14, lastDataType: 'cgm' } });

expect(screen.getByText('CGM data within 14 days')).toBeInTheDocument();
});

it('renders a CGM-use filter with its expected label', () => {
renderTray({ filters: { ...defaultFilterState, timeCGMUsePercent: '>=0.7' } });

expect(screen.getByText('≥ 70% CGM use')).toBeInTheDocument();
});
});

describe('tag chips', () => {
it('renders a "tagged" prefix and the tag name for an applied patient tag', () => {
renderTray({ filters: { ...defaultFilterState, patientTags: ['tag1'] } });

expect(screen.getByText('tagged')).toBeInTheDocument();
expect(screen.getByText('Tag One')).toBeInTheDocument();
});
});

describe('site chips', () => {
it('renders a "visiting" prefix and the site name for an applied clinic site', () => {
renderTray({ filters: { ...defaultFilterState, clinicSites: ['site1'] } });

expect(screen.getByText('visiting')).toBeInTheDocument();
expect(screen.getByText('Site Alpha')).toBeInTheDocument();
});
});

describe('removing a chip', () => {
it('fires onRemoveFilter with the chip type and value when its remove icon is clicked', async () => {
const { onRemoveFilter } = renderTray({
filters: { ...defaultFilterState, clinicSites: ['site1'] },
});

await userEvent.click(screen.getByLabelText('Remove Site Alpha filter'));

expect(onRemoveFilter).toHaveBeenCalledTimes(1);
expect(onRemoveFilter).toHaveBeenCalledWith('clinicSites', 'site1');
});
});
});
3 changes: 3 additions & 0 deletions app/core/icons/tagIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading