From 1785b26a6803826b0fa26e4c2aba11bd84e8a4d5 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 14 Jul 2026 00:55:34 -0700 Subject: [PATCH 01/13] WEB-4654 abstract out Data Recency --- app/pages/clinicworkspace/ClinicPatients.js | 124 +---------- .../clinicworkspace/FilterByDataRecency.js | 25 +++ .../components/DataRecencyFilterDropdown.js | 198 ++++++++++++++++++ 3 files changed, 229 insertions(+), 118 deletions(-) create mode 100644 app/pages/clinicworkspace/FilterByDataRecency.js create mode 100644 app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js diff --git a/app/pages/clinicworkspace/ClinicPatients.js b/app/pages/clinicworkspace/ClinicPatients.js index fa7cb877df..d864832b87 100644 --- a/app/pages/clinicworkspace/ClinicPatients.js +++ b/app/pages/clinicworkspace/ClinicPatients.js @@ -123,6 +123,7 @@ import noop from 'lodash/noop'; import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils'; import FilterByTags from './FilterByTags'; import FilterBySites from './FilterBySites'; +import FilterByDataRecency from './FilterByDataRecency'; import ClinicPatientsPrintModal from './ClinicPatientsPrintModal'; const { Loader } = vizComponents; @@ -1732,124 +1733,6 @@ export const ClinicPatients = (props) => { - - { - if (!lastDataPopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Last data filter open'), { clinicId: selectedClinicId }); - }} - sx={{ flexShrink: 0 }} - > - - - - { - trackMetric(prefixPopHealthMetric('Last upload filter close'), { clinicId: selectedClinicId }); - }} - onClose={() => { - lastDataPopupFilterState.close(); - setPendingFilters(activeFilters); - }} - > - - - - {t('Device Type')} - - - - { - setPendingFilters({ ...pendingFilters, lastDataType: event.target.value || null }); - }} - /> - - - {t('Data Recency')} - {t('Tidepool will only show patients who have data within the selected number of days.')} - - - { - setPendingFilters({ ...pendingFilters, lastData: parseInt(event.target.value) || null }); - }} - /> - - - - - - - - - { setShowClinicSitesDialog={setShowClinicSitesDialog} /> + + { if (!timeInRangePopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Time in range filter open'), { clinicId: selectedClinicId }); diff --git a/app/pages/clinicworkspace/FilterByDataRecency.js b/app/pages/clinicworkspace/FilterByDataRecency.js new file mode 100644 index 0000000000..e67540c4a0 --- /dev/null +++ b/app/pages/clinicworkspace/FilterByDataRecency.js @@ -0,0 +1,25 @@ +import React from 'react'; +import noop from 'lodash/noop'; + +import DataRecencyFilterDropdown from './components/DataRecencyFilterDropdown'; + +const FilterByDataRecency = ({ + activeFilters = {}, + setActiveFilters = noop, +}) => { + const handleChange = ({ lastData, lastDataType }) => { + setActiveFilters({ ...activeFilters, lastData, lastDataType }); + }; + + const { lastData, lastDataType } = activeFilters; + + return ( + + ); +}; + +export default FilterByDataRecency; diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js new file mode 100644 index 0000000000..93c7a42121 --- /dev/null +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -0,0 +1,198 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { trackMetric } from '../../../core/metricUtils'; + +import { Box, Text } from 'theme-ui'; +import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded'; + +import reject from 'lodash/reject'; +import noop from 'lodash/noop'; + +import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks'; + +import Button from '../../../components/elements/Button'; +import Popover from '../../../components/elements/Popover'; +import RadioGroup from '../../../components/elements/RadioGroup'; +import { Body0 } from '../../../components/elements/FontStyles'; + +import { borders } from '../../../themes/baseTheme'; +import { DialogContent, DialogActions } from '../../../components/elements/Dialog'; +import { lastDataFilterOptions } from '../../../core/clinicUtils'; + +const prefixPopHealthMetric = () => noop; // TODO: FIX + +const DropdownContent = ({ + onClose = noop, + onChange = noop, + lastData = null, + lastDataType = null, +}) => { + const { t } = useTranslation(); + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + + const [pending, setPending] = useState({ lastData, lastDataType }); + + const lastDataTypeFilterOptions = [ + { value: 'cgm', label: t('CGM') }, + { value: 'bgm', label: t('BGM') }, + ]; + + const customLastDataFilterOptions = reject(lastDataFilterOptions, { value: 7 }); + + const handleChange = (filters) => onChange(filters); + + return ( + <> + + + + {t('Device Type')} + + + + { + setPending({ ...pending, lastDataType: event.target.value || null }); + }} + /> + + + {t('Data Recency')} + {t('Tidepool will only show patients who have data within the selected number of days.')} + + + { + setPending({ ...pending, lastData: parseInt(event.target.value) || null }); + }} + /> + + + + + + + + + ); +}; + +const DataRecencyFilterDropdown = ({ + onChange = noop, + lastData = null, + lastDataType = null, +}) => { + const { t } = useTranslation(); + + const lastDataPopupFilterState = usePopupState({ + variant: 'popover', + popupId: 'lastDataFilters', + }); + + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + + const handleCloseDropdown = () => { + lastDataPopupFilterState.close(); + }; + + return ( + <> + { + if (!lastDataPopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Last data filter open'), { clinicId: selectedClinicId }); + }} + sx={{ flexShrink: 0 }} + > + + + + { + trackMetric(prefixPopHealthMetric('Last upload filter close'), { clinicId: selectedClinicId }); + }} + onClose={() => { + lastDataPopupFilterState.close(); + }} + > + { lastDataPopupFilterState.isOpen && + + } + + + ); +}; + +export default DataRecencyFilterDropdown; From cefcc39f78dd2dd377ff52d8b9d851e53d348496 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 14 Jul 2026 15:15:13 -0700 Subject: [PATCH 02/13] WEB-4654 update Data Recency visual styles --- .../components/DataRecencyFilterDropdown.js | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index 93c7a42121..bcdee081cf 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -2,8 +2,9 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { trackMetric } from '../../../core/metricUtils'; +import { colors as vizColors } from '@tidepool/viz'; -import { Box, Text } from 'theme-ui'; +import { Box, Grid, Text } from 'theme-ui'; import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded'; import reject from 'lodash/reject'; @@ -43,58 +44,54 @@ const DropdownContent = ({ const handleChange = (filters) => onChange(filters); return ( - <> - + + - + {t('Device Type')} - { - setPending({ ...pending, lastDataType: event.target.value || null }); - }} - /> - - - {t('Data Recency')} - {t('Tidepool will only show patients who have data within the selected number of days.')} + + { + setPending({ ...pending, lastDataType: event.target.value || null }); + }} + /> - { - setPending({ ...pending, lastData: parseInt(event.target.value) || null }); - }} - /> - + + {t('Data Recency')} + {t('Tidepool will only show patients who have data within the selected number of days.')} + + + + { + setPending({ ...pending, lastData: parseInt(event.target.value) || null }); + }} + /> + + - + - - + + ); }; @@ -172,7 +169,7 @@ const DataRecencyFilterDropdown = ({ { From fbb5f87cf766ae211617639fe5450be100c145b5 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 14 Jul 2026 15:29:50 -0700 Subject: [PATCH 03/13] WEB-4654 fix minor spacing issue --- .../clinicworkspace/components/DataRecencyFilterDropdown.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index bcdee081cf..fda1bd8e0b 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -78,7 +78,6 @@ const DropdownContent = ({ options={customLastDataFilterOptions} variant="vertical" sx={{ fontSize: 0 }} - mb={3} value={pending.lastData} onChange={event => { setPending({ ...pending, lastData: parseInt(event.target.value) || null }); From 453560bd68827d339924355d844dc8bb27d0deed Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 15 Jul 2026 14:46:12 -0700 Subject: [PATCH 04/13] WEB-4654 change directory for FilterByDataRecency --- app/pages/clinicworkspace/ClinicPatients.js | 2 +- .../{ => clinicPatientsFilters}/FilterByDataRecency.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/pages/clinicworkspace/{ => clinicPatientsFilters}/FilterByDataRecency.js (86%) diff --git a/app/pages/clinicworkspace/ClinicPatients.js b/app/pages/clinicworkspace/ClinicPatients.js index 713872879d..c3d238d948 100644 --- a/app/pages/clinicworkspace/ClinicPatients.js +++ b/app/pages/clinicworkspace/ClinicPatients.js @@ -122,7 +122,7 @@ import noop from 'lodash/noop'; import { getGlycemicRangesPreset } from '../../core/glycemicRangesUtils'; import FilterByTags from './clinicPatientsFilters/FilterByTags'; import FilterBySites from './clinicPatientsFilters/FilterBySites'; -import FilterByDataRecency from './FilterByDataRecency'; +import FilterByDataRecency from './clinicPatientsFilters/FilterByDataRecency'; import ClinicPatientsPrintModal from './ClinicPatientsPrintModal'; import AppliedFiltersList from './clinicPatientsFilters/AppliedFiltersList'; diff --git a/app/pages/clinicworkspace/FilterByDataRecency.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js similarity index 86% rename from app/pages/clinicworkspace/FilterByDataRecency.js rename to app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js index e67540c4a0..7c904199d3 100644 --- a/app/pages/clinicworkspace/FilterByDataRecency.js +++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js @@ -1,7 +1,7 @@ import React from 'react'; import noop from 'lodash/noop'; -import DataRecencyFilterDropdown from './components/DataRecencyFilterDropdown'; +import DataRecencyFilterDropdown from '../components/DataRecencyFilterDropdown'; const FilterByDataRecency = ({ activeFilters = {}, From a09b9c2363e5875a994af84a1f3aab270c0b9111 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 15 Jul 2026 23:01:59 -0700 Subject: [PATCH 05/13] WEB-4654 pass in custom filter options from adapter component --- .../clinicPatientsFilters/FilterByDataRecency.js | 7 +++++++ .../components/DataRecencyFilterDropdown.js | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js index 7c904199d3..bb9e18f9ab 100644 --- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js +++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js @@ -1,5 +1,9 @@ import React from 'react'; + import noop from 'lodash/noop'; +import reject from 'lodash/reject'; + +import { lastDataFilterOptions } from '../../../core/clinicUtils'; import DataRecencyFilterDropdown from '../components/DataRecencyFilterDropdown'; @@ -13,11 +17,14 @@ const FilterByDataRecency = ({ const { lastData, lastDataType } = activeFilters; + const customLastDataFilterOptions = reject(lastDataFilterOptions, { value: 7 }); + return ( ); }; diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index fda1bd8e0b..acfeeaca34 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -28,6 +28,7 @@ const DropdownContent = ({ onChange = noop, lastData = null, lastDataType = null, + filterOptions, }) => { const { t } = useTranslation(); const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); @@ -39,8 +40,6 @@ const DropdownContent = ({ { value: 'bgm', label: t('BGM') }, ]; - const customLastDataFilterOptions = reject(lastDataFilterOptions, { value: 7 }); - const handleChange = (filters) => onChange(filters); return ( @@ -75,7 +74,7 @@ const DropdownContent = ({ { const { t } = useTranslation(); @@ -182,6 +182,7 @@ const DataRecencyFilterDropdown = ({ From 1a1180aca6d487567d2700b23b4d79e6125ed0e6 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 15 Jul 2026 23:36:31 -0700 Subject: [PATCH 06/13] WEB-4654 clear unneeded props --- .../components/DataRecencyFilterDropdown.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index acfeeaca34..ab2350ca4e 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -24,10 +24,10 @@ import { lastDataFilterOptions } from '../../../core/clinicUtils'; const prefixPopHealthMetric = () => noop; // TODO: FIX const DropdownContent = ({ - onClose = noop, - onChange = noop, - lastData = null, - lastDataType = null, + onClose, + onChange, + lastData, + lastDataType, filterOptions, }) => { const { t } = useTranslation(); @@ -174,9 +174,7 @@ const DataRecencyFilterDropdown = ({ onClickCloseIcon={() => { trackMetric(prefixPopHealthMetric('Last upload filter close'), { clinicId: selectedClinicId }); }} - onClose={() => { - lastDataPopupFilterState.close(); - }} + onClose={handleCloseDropdown} > { lastDataPopupFilterState.isOpen && Date: Thu, 16 Jul 2026 13:32:28 -0700 Subject: [PATCH 07/13] WEB-4654 add tests for Data Recency filter --- .../clinicworkspace/ClinicPatients.test.js | 28 ++++ .../components/DataRecencyFilterDropdown.js | 17 ++- test/unit/pages/ClinicPatients.test.js | 53 ------- .../FilterByDataRecency.test.js | 94 ++++++++++++ .../DataRecencyFilterDropdown.test.js | 142 ++++++++++++++++++ 5 files changed, 273 insertions(+), 61 deletions(-) create mode 100644 test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js create mode 100644 test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js diff --git a/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js b/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js index 2f495b8929..9557f5f23d 100644 --- a/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/ClinicPatients.test.js @@ -424,6 +424,34 @@ describe('ClinicPatients', () => { expect.any(Function), ); }, TEST_TIMEOUT_MS); + + it('maps an applied data recency filter into the getPatientsForClinic query', async () => { + render( + + + + ); + + // Open the Data Recency filter dropdown, pick a device type and window, and apply. + // Match the trigger via its icon label ("Data Recency" alone also matches the + // sortable column header of the same name). + await userEvent.click(screen.getByRole('button', { name: /Filter by last upload/ })); + await userEvent.click(screen.getByRole('radio', { name: /CGM/ })); + await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ })); + await userEvent.click(screen.getByRole('button', { name: /Apply/ })); + + // The from/to date bounds are derived from the current date, so assert their + // presence and 14-day span rather than exact ISO timestamps. + expect(defaultProps.api.clinics.getPatientsForClinic).toHaveBeenLastCalledWith( + 'clinicID123', + expect.objectContaining({ + 'cgm.lastDataFrom': expect.any(String), + 'cgm.lastDataTo': expect.any(String), + limit: 50, offset: 0, period: '14d', sortType: 'cgm', sort: '-lastData', + }), + expect.any(Function), + ); + }, TEST_TIMEOUT_MS); }); describe('managing sites', () => { diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index ab2350ca4e..127e6a693f 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -20,8 +20,7 @@ import { Body0 } from '../../../components/elements/FontStyles'; import { borders } from '../../../themes/baseTheme'; import { DialogContent, DialogActions } from '../../../components/elements/Dialog'; import { lastDataFilterOptions } from '../../../core/clinicUtils'; - -const prefixPopHealthMetric = () => noop; // TODO: FIX +import useClinicMetricsPageName from '../useClinicMetricsPageName'; const DropdownContent = ({ onClose, @@ -32,6 +31,7 @@ const DropdownContent = ({ }) => { const { t } = useTranslation(); const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + const pageName = useClinicMetricsPageName(); const [pending, setPending] = useState({ lastData, lastDataType }); @@ -43,7 +43,7 @@ const DropdownContent = ({ const handleChange = (filters) => onChange(filters); return ( - + @@ -91,7 +91,7 @@ const DropdownContent = ({ sx={{ fontSize: 1 }} variant="secondary" onClick={() => { - trackMetric(prefixPopHealthMetric('Last upload clear filter'), { clinicId: selectedClinicId }); + trackMetric('Clinic - Last upload clear filter', { clinicId: selectedClinicId, pageName }); setPending({ lastData: null, lastDataType: null }); handleChange({ lastData: null, lastDataType: null }); onClose(); @@ -110,7 +110,7 @@ const DropdownContent = ({ ? 'today' : `${pending.lastData} days`; - trackMetric(prefixPopHealthMetric('Last upload apply filter'), { + trackMetric('Clinic - Last upload apply filter', { clinicId: selectedClinicId, dateRange, type: pending.lastDataType, @@ -134,6 +134,7 @@ const DataRecencyFilterDropdown = ({ filterOptions = lastDataFilterOptions, }) => { const { t } = useTranslation(); + const pageName = useClinicMetricsPageName(); const lastDataPopupFilterState = usePopupState({ variant: 'popover', @@ -150,7 +151,7 @@ const DataRecencyFilterDropdown = ({ <> { - if (!lastDataPopupFilterState.isOpen) trackMetric(prefixPopHealthMetric('Last data filter open'), { clinicId: selectedClinicId }); + if (!lastDataPopupFilterState.isOpen) trackMetric('Clinic - Last data filter open', { clinicId: selectedClinicId, pageName }); }} sx={{ flexShrink: 0 }} > @@ -172,7 +173,7 @@ const DataRecencyFilterDropdown = ({ closeIcon {...bindPopover(lastDataPopupFilterState)} onClickCloseIcon={() => { - trackMetric(prefixPopHealthMetric('Last upload filter close'), { clinicId: selectedClinicId }); + trackMetric('Clinic - Last upload filter close', { clinicId: selectedClinicId, pageName }); }} onClose={handleCloseDropdown} > @@ -180,7 +181,7 @@ const DataRecencyFilterDropdown = ({ diff --git a/test/unit/pages/ClinicPatients.test.js b/test/unit/pages/ClinicPatients.test.js index 84899dc057..9e428e7700 100644 --- a/test/unit/pages/ClinicPatients.test.js +++ b/test/unit/pages/ClinicPatients.test.js @@ -1639,54 +1639,6 @@ describe('ClinicPatients', () => { expect(timeAgoMessage).to.equal('Last updated less than an hour ago'); }); - it('should allow filtering by last upload', () => { - const lastDataFilterTrigger = container.querySelector('#last-data-filter-trigger'); - expect(lastDataFilterTrigger).to.exist; - - const popover = () => document.querySelector('#lastDataFilters'); - expect(popover()).to.exist; - expect(popover().style.visibility).to.equal('hidden'); - - // Open filters popover - fireEvent.click(lastDataFilterTrigger); - expect(popover().style.visibility).to.equal(''); - - // Ensure filter options present - const typeFilterOptions = document.querySelectorAll('#last-upload-type label'); - expect(typeFilterOptions.length).to.equal(2); - expect(typeFilterOptions[0].textContent).to.equal('CGM'); - expect(typeFilterOptions[0].querySelector('input').value).to.equal('cgm'); - - expect(typeFilterOptions[1].textContent).to.equal('BGM'); - expect(typeFilterOptions[1].querySelector('input').value).to.equal('bgm'); - - // Ensure period filter options present - const periodFilterOptions = document.querySelectorAll('#last-upload-filters label'); - expect(periodFilterOptions.length).to.equal(4); - expect(periodFilterOptions[0].textContent).to.equal('Today'); - expect(periodFilterOptions[0].querySelector('input').value).to.equal('1'); - - expect(periodFilterOptions[1].textContent).to.equal('Within 2 days'); - expect(periodFilterOptions[1].querySelector('input').value).to.equal('2'); - - expect(periodFilterOptions[2].textContent).to.equal('Within 14 days'); - expect(periodFilterOptions[2].querySelector('input').value).to.equal('14'); - - expect(periodFilterOptions[3].textContent).to.equal('Within 30 days'); - expect(periodFilterOptions[3].querySelector('input').value).to.equal('30'); - - // Apply button disabled until selection made - const applyButton = () => document.querySelector('#apply-last-upload-filter'); - expect(applyButton().disabled).to.be.true; - - fireEvent.click(typeFilterOptions[1].querySelector('input')); - fireEvent.click(periodFilterOptions[3].querySelector('input')); - expect(applyButton().disabled).to.be.false; - - fireEvent.click(applyButton()); - sinon.assert.calledWith(defaultProps.trackMetric, 'Clinic - Population Health - Last upload apply filter', sinon.match({ clinicId: 'clinicID123', dateRange: '30 days', type: 'bgm'})); - }); - it('should allow filtering by cgm use', () => { const cgmUseFilterTrigger = container.querySelector('#cgm-use-filter-trigger'); expect(cgmUseFilterTrigger).to.exist; @@ -2010,11 +1962,6 @@ describe('ClinicPatients', () => { defaultProps.trackMetric.resetHistory(); }); - it('should set the last upload filter on load based on the stored filters', () => { - const lastDataFilterTrigger = container.querySelector('#last-data-filter-trigger'); - expect(lastDataFilterTrigger.textContent).to.equal('Data within 14 days'); - }); - it('should set the patient tag filters on load based on the stored filters', () => { const patientTagsFilterCount = container.querySelector('#patient-tags-filter-count'); expect(patientTagsFilterCount.textContent).to.equal('1'); diff --git a/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js b/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js new file mode 100644 index 0000000000..4865d907bd --- /dev/null +++ b/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { thunk } from 'redux-thunk'; + +import FilterByDataRecency from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency'; +import useClinicMetricsPageName from '@app/pages/clinicworkspace/useClinicMetricsPageName'; + +jest.mock('@app/pages/clinicworkspace/useClinicMetricsPageName'); + +const mockStore = configureStore([thunk]); + +describe('FilterByDataRecency', () => { + let store; + let wrapper; + + const selectedClinicId = 'clinic123'; + + const setActiveFilters = jest.fn(); + + useClinicMetricsPageName.mockReturnValue('Population Health'); + + const ui = (props = {}) => ( + + + + ); + + const renderComponent = (props = {}) => render(ui(props)); + + beforeEach(() => { + store = mockStore({ + blip: { + selectedClinicId, + clinics: { [selectedClinicId]: { id: selectedClinicId } }, + }, + }); + + setActiveFilters.mockClear(); + }); + + describe('handleChange', () => { + it('calls setActiveFilters with the applied data recency merged into the existing activeFilters', async () => { + wrapper = renderComponent({ activeFilters: { lastData: null, lastDataType: null, patientTags: ['tag1'] } }); + + await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ })); + await screen.findByTestId('data-recency-filter-dropdown'); + + await userEvent.click(screen.getByRole('radio', { name: /CGM/ })); + await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ })); + await userEvent.click(screen.getByRole('button', { name: 'Apply' })); + + expect(setActiveFilters).toHaveBeenCalledTimes(1); + expect(setActiveFilters).toHaveBeenCalledWith({ + lastData: 14, + lastDataType: 'cgm', + patientTags: ['tag1'], + }); + }); + }); + + describe('filterOptions', () => { + it('omits the 7-day option from the data recency choices', async () => { + wrapper = renderComponent(); + + await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ })); + await screen.findByTestId('data-recency-filter-dropdown'); + + expect(screen.getByRole('radio', { name: /Today/ })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /Within 2 days/ })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeInTheDocument(); + expect(screen.queryByRole('radio', { name: /Within 7 days/ })).not.toBeInTheDocument(); + }); + }); + + describe('activeFilters passthrough', () => { + it('reflects the active data recency in the pre-selected radios', async () => { + wrapper = renderComponent({ activeFilters: { lastData: 30, lastDataType: 'bgm' } }); + + await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ })); + await screen.findByTestId('data-recency-filter-dropdown'); + + expect(screen.getByRole('radio', { name: /BGM/ })).toBeChecked(); + expect(screen.getByRole('radio', { name: /CGM/ })).not.toBeChecked(); + expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeChecked(); + }); + }); +}); diff --git a/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js b/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js new file mode 100644 index 0000000000..d4c8c4d806 --- /dev/null +++ b/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js @@ -0,0 +1,142 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { thunk } from 'redux-thunk'; + +import * as actions from '@app/redux/actions'; +import DataRecencyFilterDropdown from '@app/pages/clinicworkspace/components/DataRecencyFilterDropdown'; +import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils'; +import useClinicMetricsPageName from '@app/pages/clinicworkspace/useClinicMetricsPageName'; + +jest.mock('@app/pages/clinicworkspace/useClinicMetricsPageName'); + +const mockStore = configureStore([thunk]); + +describe('DataRecencyFilterDropdown', () => { + let store; + let wrapper; + + const selectedClinicId = 'clinic123'; + + const filterOptions = [ + { value: 1, label: 'Today' }, + { value: 2, label: 'Within 2 days' }, + { value: 14, label: 'Within 14 days' }, + { value: 30, label: 'Within 30 days' }, + ]; + + let onChange = jest.fn(); + + useClinicMetricsPageName.mockReturnValue('Population Health'); + + const ui = (props = {}) => ( + + + + ); + + const renderComponent = (props = {}) => render(ui(props)); + + beforeEach(() => { + store = mockStore({ + blip: { + selectedClinicId, + clinics: { [selectedClinicId]: { id: selectedClinicId } }, + }, + }); + + onChange.mockClear(); + mockTrackMetric.mockClear(); + }); + + describe('filtering for data recency', () => { + it('applies the device type and data recency based on radios selected', async () => { + renderComponent(); + + // Dropdown closed initially + expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument(); + + // Open the dropdown + await userEvent.click(screen.getByRole('button', { name: /Data Recency/ })); + expect(screen.getByTestId('data-recency-filter-dropdown')).toBeInTheDocument(); + + // Nothing selected initially + expect(screen.getByRole('radio', { name: /CGM/ })).not.toBeChecked(); + expect(screen.getByRole('radio', { name: /BGM/ })).not.toBeChecked(); + expect(screen.getByRole('radio', { name: /Within 14 days/ })).not.toBeChecked(); + + // Select a device type and a data recency window + await userEvent.click(screen.getByRole('radio', { name: /CGM/ })); + await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ })); + expect(screen.getByRole('radio', { name: /CGM/ })).toBeChecked(); + expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeChecked(); + + // Applying the filter sets it + await userEvent.click(screen.getByRole('button', { name: /Apply/ })); + expect(onChange).toHaveBeenCalledWith({ lastData: 14, lastDataType: 'cgm' }); + expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload apply filter', { clinicId: 'clinic123', dateRange: '14 days', type: 'cgm' }); + + // Dropdown should automatically close + expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument(); + }); + + it('disables the Apply button until both a device type and data recency are selected', async () => { + renderComponent(); + + await userEvent.click(screen.getByRole('button', { name: /Data Recency/ })); + + // Disabled with nothing selected + expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled(); + + // Still disabled with only a device type selected + await userEvent.click(screen.getByRole('radio', { name: /BGM/ })); + expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled(); + + // Enabled once a data recency window is also selected + await userEvent.click(screen.getByRole('radio', { name: /Within 2 days/ })); + expect(screen.getByRole('button', { name: /Apply/ })).toBeEnabled(); + }); + + it('reports the "today" date range when the single-day option is applied', async () => { + renderComponent(); + + await userEvent.click(screen.getByRole('button', { name: /Data Recency/ })); + + await userEvent.click(screen.getByRole('radio', { name: /BGM/ })); + await userEvent.click(screen.getByRole('radio', { name: /Today/ })); + await userEvent.click(screen.getByRole('button', { name: /Apply/ })); + + expect(onChange).toHaveBeenCalledWith({ lastData: 1, lastDataType: 'bgm' }); + expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload apply filter', { clinicId: 'clinic123', dateRange: 'today', type: 'bgm' }); + }); + + it('pre-selects the radios matching the active filters', async () => { + renderComponent({ lastData: 30, lastDataType: 'cgm' }); + + await userEvent.click(screen.getByRole('button', { name: /Data Recency/ })); + + expect(screen.getByRole('radio', { name: /CGM/ })).toBeChecked(); + expect(screen.getByRole('radio', { name: /BGM/ })).not.toBeChecked(); + expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeChecked(); + }); + + it('clears the filter', async () => { + renderComponent({ lastData: 14, lastDataType: 'cgm' }); + + await userEvent.click(screen.getByRole('button', { name: /Data Recency/ })); + expect(screen.getByRole('radio', { name: /CGM/ })).toBeChecked(); + expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeChecked(); + + await userEvent.click(screen.getByRole('button', { name: /Clear/ })); + expect(onChange).toHaveBeenCalledWith({ lastData: null, lastDataType: null }); + expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload clear filter', { clinicId: 'clinic123', pageName: 'Population Health' }); + expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument(); + }); + }); +}); From 675d246e1e709fb49dfd345d48420dccd0cee15e Mon Sep 17 00:00:00 2001 From: henry-tp Date: Fri, 17 Jul 2026 09:38:44 -0700 Subject: [PATCH 08/13] WEB-4654 remove unnecessary tests --- .../FilterByDataRecency.test.js | 22 ++++++------------- .../DataRecencyFilterDropdown.test.js | 15 ------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js b/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js index 4865d907bd..061a4a8546 100644 --- a/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js +++ b/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js @@ -51,6 +51,13 @@ describe('FilterByDataRecency', () => { await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ })); await screen.findByTestId('data-recency-filter-dropdown'); + // Correct options exist + expect(screen.getByRole('radio', { name: /Today/ })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /Within 2 days/ })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeInTheDocument(); + expect(screen.queryByRole('radio', { name: /Within 7 days/ })).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole('radio', { name: /CGM/ })); await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ })); await userEvent.click(screen.getByRole('button', { name: 'Apply' })); @@ -64,21 +71,6 @@ describe('FilterByDataRecency', () => { }); }); - describe('filterOptions', () => { - it('omits the 7-day option from the data recency choices', async () => { - wrapper = renderComponent(); - - await userEvent.click(screen.getByRole('button', { name: /^Data Recency/ })); - await screen.findByTestId('data-recency-filter-dropdown'); - - expect(screen.getByRole('radio', { name: /Today/ })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Within 2 days/ })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: /Within 30 days/ })).toBeInTheDocument(); - expect(screen.queryByRole('radio', { name: /Within 7 days/ })).not.toBeInTheDocument(); - }); - }); - describe('activeFilters passthrough', () => { it('reflects the active data recency in the pre-selected radios', async () => { wrapper = renderComponent({ activeFilters: { lastData: 30, lastDataType: 'bgm' } }); diff --git a/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js b/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js index d4c8c4d806..db5db40bd6 100644 --- a/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js +++ b/test/unit/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js @@ -74,8 +74,6 @@ describe('DataRecencyFilterDropdown', () => { // Select a device type and a data recency window await userEvent.click(screen.getByRole('radio', { name: /CGM/ })); await userEvent.click(screen.getByRole('radio', { name: /Within 14 days/ })); - expect(screen.getByRole('radio', { name: /CGM/ })).toBeChecked(); - expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeChecked(); // Applying the filter sets it await userEvent.click(screen.getByRole('button', { name: /Apply/ })); @@ -103,19 +101,6 @@ describe('DataRecencyFilterDropdown', () => { expect(screen.getByRole('button', { name: /Apply/ })).toBeEnabled(); }); - it('reports the "today" date range when the single-day option is applied', async () => { - renderComponent(); - - await userEvent.click(screen.getByRole('button', { name: /Data Recency/ })); - - await userEvent.click(screen.getByRole('radio', { name: /BGM/ })); - await userEvent.click(screen.getByRole('radio', { name: /Today/ })); - await userEvent.click(screen.getByRole('button', { name: /Apply/ })); - - expect(onChange).toHaveBeenCalledWith({ lastData: 1, lastDataType: 'bgm' }); - expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload apply filter', { clinicId: 'clinic123', dateRange: 'today', type: 'bgm' }); - }); - it('pre-selects the radios matching the active filters', async () => { renderComponent({ lastData: 30, lastDataType: 'cgm' }); From b8087c99232390321b3a793d114b886b849dc489 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Sun, 19 Jul 2026 14:55:32 -0700 Subject: [PATCH 09/13] WEB-4654 use MemoryRouter instead of mocking out location hook --- .../FilterByDataRecency.test.js | 16 +++++++--------- .../DataRecencyFilterDropdown.test.js | 18 ++++++++---------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js index 061a4a8546..2d3c9210e7 100644 --- a/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.test.js @@ -4,11 +4,9 @@ import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import { thunk } from 'redux-thunk'; +import { MemoryRouter } from 'react-router-dom'; import FilterByDataRecency from '@app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency'; -import useClinicMetricsPageName from '@app/pages/clinicworkspace/useClinicMetricsPageName'; - -jest.mock('@app/pages/clinicworkspace/useClinicMetricsPageName'); const mockStore = configureStore([thunk]); @@ -20,14 +18,14 @@ describe('FilterByDataRecency', () => { const setActiveFilters = jest.fn(); - useClinicMetricsPageName.mockReturnValue('Population Health'); - const ui = (props = {}) => ( - + + + ); diff --git a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js index db5db40bd6..e979197640 100644 --- a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js @@ -4,13 +4,11 @@ import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import { thunk } from 'redux-thunk'; +import { MemoryRouter } from 'react-router-dom'; import * as actions from '@app/redux/actions'; import DataRecencyFilterDropdown from '@app/pages/clinicworkspace/components/DataRecencyFilterDropdown'; import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils'; -import useClinicMetricsPageName from '@app/pages/clinicworkspace/useClinicMetricsPageName'; - -jest.mock('@app/pages/clinicworkspace/useClinicMetricsPageName'); const mockStore = configureStore([thunk]); @@ -29,15 +27,15 @@ describe('DataRecencyFilterDropdown', () => { let onChange = jest.fn(); - useClinicMetricsPageName.mockReturnValue('Population Health'); - const ui = (props = {}) => ( - + + + ); From c3843248e46e2747c5f728463e626b30a11fb41e Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 20 Jul 2026 15:43:21 -0700 Subject: [PATCH 10/13] WEB-4654 remove unused var --- app/pages/clinicworkspace/ClinicPatients.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/pages/clinicworkspace/ClinicPatients.js b/app/pages/clinicworkspace/ClinicPatients.js index 4251ef57e3..655648775b 100644 --- a/app/pages/clinicworkspace/ClinicPatients.js +++ b/app/pages/clinicworkspace/ClinicPatients.js @@ -729,8 +729,6 @@ export const ClinicPatients = (props) => { { value: 'bgm', label: t('BGM') }, ]; - const customLastDataFilterOptions = reject(lastDataFilterOptions, { value: 7 }); - const summaryPeriodOptions = [ { value: '1d', label: t('24 hours') }, { value: '7d', label: t('7 days') }, From 66bc00fe3d931ed7fd42a6bfc010ae9df7bde284 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 20 Jul 2026 15:45:53 -0700 Subject: [PATCH 11/13] WEB-4654 remove unused var --- app/pages/clinicworkspace/ClinicPatients.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/pages/clinicworkspace/ClinicPatients.js b/app/pages/clinicworkspace/ClinicPatients.js index 655648775b..fd268783d7 100644 --- a/app/pages/clinicworkspace/ClinicPatients.js +++ b/app/pages/clinicworkspace/ClinicPatients.js @@ -724,11 +724,6 @@ export const ClinicPatients = (props) => { { value: '>=0.7', label: t('70% or more') }, ]; - const lastDataTypeFilterOptions = [ - { value: 'cgm', label: t('CGM') }, - { value: 'bgm', label: t('BGM') }, - ]; - const summaryPeriodOptions = [ { value: '1d', label: t('24 hours') }, { value: '7d', label: t('7 days') }, From b48f56c1d9b847d9cb1c1ef4c513f6e47af4b4aa Mon Sep 17 00:00:00 2001 From: henry-tp Date: Sat, 25 Jul 2026 21:50:22 -0700 Subject: [PATCH 12/13] WEB-4654 update pageName --- .../clinicworkspace/components/DataRecencyFilterDropdown.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index 127e6a693f..0c6e706de7 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -114,6 +114,7 @@ const DropdownContent = ({ clinicId: selectedClinicId, dateRange, type: pending.lastDataType, + pageName, }); handleChange(pending); From 990ffa47a823da20578d1acee26f36ef6ec466e4 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Sat, 25 Jul 2026 23:01:23 -0700 Subject: [PATCH 13/13] WEB-4654 fix tests --- .../components/DataRecencyFilterDropdown.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js index e979197640..a0566fb0e1 100644 --- a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js @@ -76,7 +76,7 @@ describe('DataRecencyFilterDropdown', () => { // Applying the filter sets it await userEvent.click(screen.getByRole('button', { name: /Apply/ })); expect(onChange).toHaveBeenCalledWith({ lastData: 14, lastDataType: 'cgm' }); - expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload apply filter', { clinicId: 'clinic123', dateRange: '14 days', type: 'cgm' }); + expect(mockTrackMetric).toHaveBeenCalledWith('Clinic - Last upload apply filter', { clinicId: 'clinic123', dateRange: '14 days', type: 'cgm', pageName: 'Population Health' }); // Dropdown should automatically close expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument();