diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js
index 78afacb932..39514ce732 100644
--- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js
@@ -1,7 +1,8 @@
import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router-dom';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import isEqual from 'lodash/isEqual';
@@ -11,7 +12,6 @@ import { setupStore } from '@tests/utils/setupStore';
import blipReducer from '@app/redux/reducers';
import { CATEGORY } from '@app/pages/clinicworkspace/TideDashboardV2/tideDashboardSlice';
import TideDashboardV2 from '@app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2';
-import { MemoryRouter } from 'react-router-dom';
// Pin the data recency window to a stable [lastDataFrom, lastDataTo]
jest.mock('@app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints', () => ({
@@ -119,9 +119,18 @@ const anticipatedQueries = {
'cgm.timeInVeryHighPercent': '<0.045',
'cgm.timeCGMUsePercent': '>=0.695',
},
+ 'DEFAULT_WITH_FILTERS': {
+ offset: '0',
+ limit: '12',
+ period: '30d',
+ 'cgm.lastDataFrom': '2025-05-23T00:00:00.000Z',
+ 'cgm.lastDataTo': '2025-05-30T00:00:00.000Z',
+ tags: 'tag8',
+ sites: 'site9',
+ },
};
-const patientsForCategory = {
+const datasets = {
[DEFAULT]: [
{ id: 'default-1', fullName: 'Default Patient 1', birthDate: '2001-01-01' },
{ id: 'default-2', fullName: 'Default Patient 2', birthDate: '2002-02-02' },
@@ -154,22 +163,25 @@ const patientsForCategory = {
{ id: 'target-1', fullName: 'Meeting Targets Patient 1', birthDate: '2015-03-15' },
{ id: 'target-2', fullName: 'Meeting Targets Patient 2', birthDate: '2016-04-16' },
],
+ 'DEFAULT_WITH_FILTERS': [
+ { id: 'filtered-3', fullName: 'Filtered Patient 3', birthDate: '2001-01-01' },
+ ],
};
const getCorrespondingDataForQuery = (searchParams) => {
- let datasetName;
+ let matchedDatasetName;
// Look over anticipated queries. If the query matches, return the dataset
- for (let [category, anticipatedQuery] of entries(anticipatedQueries)) {
+ for (let [datasetName, anticipatedQuery] of entries(anticipatedQueries)) {
if (isEqual(searchParams, anticipatedQuery)) {
- datasetName = category;
+ matchedDatasetName = datasetName;
break;
}
}
- if (!datasetName) throw new Error('No data for this query found');
+ if (!matchedDatasetName) throw new Error('No data for this query found');
- return patientsForCategory[datasetName];
+ return datasets[matchedDatasetName];
};
const server = setupServer(
@@ -263,4 +275,64 @@ describe('TideDashboardV2', () => {
expect(screen.getByText('Meeting Targets Patient 2')).toBeInTheDocument();
expect(screen.queryByText('Low CGM Wear Patient 1')).not.toBeInTheDocument();
}, TEST_TIMEOUT_MS);
+
+ it('fetches with filters', async () => {
+ store = setupStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics: { clinic123: { id: 'clinic123', patientTags: [{ id: 'tag8', name: 'Tag 8' }], sites: [{ id: 'site9', name: 'Site 9' }] } },
+ tideDashboardFilters: { lastData: 7, patientTags: ['tag8'], clinicSites: ['site9'], summaryPeriod: '30d' },
+ },
+ }, { blip: blipReducer });
+
+ renderComponent();
+
+ expect(await screen.findByText('Filtered Patient 3')).toBeInTheDocument();
+ }, TEST_TIMEOUT_MS);
+
+ describe('empty content', () => {
+ beforeEach(() => {
+ let fetchCount = 0;
+
+ server.use(
+ http.get('http://app.tidepool.test/v1/clinics/clinic123/patients', () => {
+ fetchCount += 1;
+
+ return fetchCount <= 1
+ // first fetch -> no patients
+ ? HttpResponse.json({ data: [], meta: { count: 0 } })
+ // refetch -> 1 patient
+ : HttpResponse.json({ data: [{ id: 'default-1', fullName: 'Default Patient 1', birthDate: '2001-01-01' }], meta: { count: 1 } });
+ }
+ )
+ );
+ });
+
+ it('shows Reset button when no patients match applied filters', async () => {
+ // A tag filter is applied before the dashboard mounts
+ store = setupStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics: { clinic123: { id: 'clinic123', patientTags: [{ id: 'tag1', name: 'Tag One' }] } },
+ tideDashboardFilters: { lastData: 7, patientTags: ['tag1'], clinicSites: [], summaryPeriod: '14d' },
+ },
+ }, { blip: blipReducer });
+
+ renderComponent();
+
+ const emptyContent = await screen.findByTestId('tide-dashboard-empty-content');
+ expect(within(emptyContent).getByText('There are no patients with the current filter(s)')).toBeInTheDocument();
+ await userEvent.click(within(emptyContent).getByRole('button', { name: 'Reset All Filters' }));
+
+ expect(await screen.findByText('Default Patient 1')).toBeInTheDocument();
+ });
+
+ it('shows an empty message without a reset button when there are no patients and no filters applied', async () => {
+ renderComponent();
+
+ const emptyContent = await screen.findByTestId('tide-dashboard-empty-content');
+ expect(within(emptyContent).getByText('There are no results to show')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Reset All Filters' })).not.toBeInTheDocument(); // no filters = no button
+ });
+ });
});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency.test.js
new file mode 100644
index 0000000000..957d47e337
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency.test.js
@@ -0,0 +1,64 @@
+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 { MemoryRouter } from 'react-router-dom';
+
+import FilterByDataRecency from '@app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterByDataRecency', () => {
+ let store;
+
+ const renderComponent = () => render(
+
+
+
+
+
+ );
+
+ it('dispatches the lastData filter and offset reset when data recency filter is applied', async () => {
+ store = mockStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ tideDashboardFilters: {
+ lastData: 7,
+ patientTags: [],
+ clinicSites: [],
+ summaryPeriod: '14d',
+ },
+ },
+ });
+
+ renderComponent();
+
+ // Open the dropdown
+ expect(screen.queryByTestId('data-recency-filter-dropdown')).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: /Data Recency/ }));
+ expect(screen.getByTestId('data-recency-filter-dropdown')).toBeInTheDocument();
+
+ expect(screen.queryByRole('radio', { name: /CGM/ })).not.toBeInTheDocument(); // not selectable in TIDE
+ expect(screen.queryByRole('radio', { name: /BGM/ })).not.toBeInTheDocument(); // not selectable in TIDE
+
+ expect(screen.getByRole('radio', { name: /Today/ })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /Within 2 days/ })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /Within 7 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /Within 14 days/ })).toBeInTheDocument();
+ expect(screen.queryByRole('radio', { name: /Within 30 days/ })).not.toBeInTheDocument();
+
+ // Select an option
+ await userEvent.click(screen.getByRole('radio', { name: /Within 2 days/ }));
+ expect(store.getActions()).toStrictEqual([]);
+
+ // Applying the filter dispatches the new filter value and resets the page offset
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setLastDataFilter', payload: 2 },
+ { type: 'tideDashboard/setOffset', payload: 0 },
+ ]);
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites.test.js
new file mode 100644
index 0000000000..8ef018fac4
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites.test.js
@@ -0,0 +1,61 @@
+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 { MemoryRouter } from 'react-router-dom';
+
+import FilterBySites from '@app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterBySites', () => {
+ let store;
+
+ const renderComponent = () => render(
+
+
+
+
+
+ );
+
+ it('dispatches the clinic sites filter and an offset reset when a site filter is applied', async () => {
+ store = mockStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics: {
+ clinic123: {
+ id: 'clinic123',
+ sites: [{ id: 'site1', name: 'Site Alpha' }, { id: 'site2', name: 'Site Bravo' }],
+ },
+ },
+ tideDashboardFilters: {
+ lastData: 7,
+ patientTags: [],
+ clinicSites: ['site1'],
+ summaryPeriod: '14d',
+ },
+ },
+ });
+
+ renderComponent();
+
+ // Open the dropdown
+ expect(screen.queryByTestId('site-filter-dropdown')).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: /Clinic Sites/ }));
+ expect(screen.getByTestId('site-filter-dropdown')).toBeInTheDocument();
+
+ // Selecting a site
+ await userEvent.click(screen.getByRole('checkbox', { name: /Site Bravo/ }));
+ expect(store.getActions()).toStrictEqual([]);
+
+ // Applying the filter dispatches the selected sites and resets the page offset
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setClinicSitesFilter', payload: ['site1', 'site2'] },
+ { type: 'tideDashboard/setOffset', payload: 0 },
+ ]);
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod.test.js
new file mode 100644
index 0000000000..cd052c96d5
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod.test.js
@@ -0,0 +1,59 @@
+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 { MemoryRouter } from 'react-router-dom';
+
+import FilterBySummaryPeriod from '@app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterBySummaryPeriod', () => {
+ let store;
+
+ const renderComponent = () => render(
+
+
+
+
+
+ );
+
+ it('dispatches the summary period filter and an offset reset when a summary period is applied', async () => {
+ store = mockStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ tideDashboardFilters: {
+ lastData: 7,
+ patientTags: [],
+ clinicSites: [],
+ summaryPeriod: '14d' },
+ },
+ });
+
+ renderComponent();
+
+ // Open the dropdown
+ expect(screen.queryByTestId('summary-period-filter-dropdown')).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: /Summarizing 14 days of data/ }));
+
+ expect(screen.getByTestId('summary-period-filter-dropdown')).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /24 hours/ })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /7 days/ })).toBeInTheDocument();
+ expect(screen.getByRole('radio', { name: /14 days/ })).toBeChecked();
+ expect(screen.getByRole('radio', { name: /30 days/ })).toBeInTheDocument();
+
+ // Selecting a period
+ await userEvent.click(screen.getByRole('radio', { name: /30 days/ }));
+ expect(store.getActions()).toStrictEqual([]);
+
+ // Applying the filter dispatches the new period and resets the page offset
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setSummaryPeriodFilter', payload: '30d' },
+ { type: 'tideDashboard/setOffset', payload: 0 },
+ ]);
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags.test.js
new file mode 100644
index 0000000000..ac0e8ac3a7
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags.test.js
@@ -0,0 +1,64 @@
+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 { MemoryRouter } from 'react-router-dom';
+
+import FilterByTags from '@app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags';
+
+const mockStore = configureStore([thunk]);
+
+describe('FilterByTags', () => {
+ let store;
+
+ const renderComponent = () => render(
+
+
+
+
+
+ );
+
+ it('dispatches the patient tags filter and an offset reset when a tag filter is applied', async () => {
+ store = mockStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics: {
+ clinic123: {
+ id: 'clinic123',
+ patientTags: [
+ { id: 'tag1', name: 'Week 1' },
+ { id: 'tag2', name: 'Week 2' },
+ ],
+ },
+ },
+ tideDashboardFilters: {
+ lastData: 7,
+ patientTags: [],
+ clinicSites: [],
+ summaryPeriod: '14d',
+ },
+ },
+ });
+
+ renderComponent();
+
+ // Open the dropdown
+ expect(screen.queryByTestId('tag-filter-dropdown')).not.toBeInTheDocument();
+ await userEvent.click(screen.getByRole('button', { name: /Tags/ }));
+ expect(screen.getByTestId('tag-filter-dropdown')).toBeInTheDocument();
+
+ // Selecting a tag
+ await userEvent.click(screen.getByRole('checkbox', { name: /Week 2/ }));
+ expect(store.getActions()).toStrictEqual([]);
+
+ // Applying the filter dispatches the selected tags and resets the page offset
+ await userEvent.click(screen.getByRole('button', { name: /Apply/ }));
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setPatientTagsFilter', payload: ['tag2'] },
+ { type: 'tideDashboard/setOffset', payload: 0 },
+ ]);
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice.test.js
new file mode 100644
index 0000000000..7595e45e82
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice.test.js
@@ -0,0 +1,41 @@
+/* global describe */
+/* global it */
+/* global expect */
+
+import reducer from '@app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice';
+
+describe('tideDashboardFiltersSlice', () => {
+ const defaultFilters = { lastData: 7, patientTags: [], clinicSites: [], summaryPeriod: '14d' };
+ const appliedFilters = { lastData: 30, patientTags: ['tag1'], clinicSites: ['site1'], summaryPeriod: '30d' };
+
+ describe('SELECT_CLINIC_SUCCESS', () => {
+ it('should hydrate the filters carried on the clinic selection', () => {
+ const persistedFilters = { lastData: 14, patientTags: ['tag2'], clinicSites: [], summaryPeriod: '7d' };
+
+ const state = reducer(appliedFilters, {
+ type: 'SELECT_CLINIC_SUCCESS',
+ payload: { clinicId: 'clinic123', tideDashboardFilters: persistedFilters },
+ });
+
+ expect(state).toStrictEqual(persistedFilters);
+ });
+
+ it('should reset to the default filters when the clinic selection carries none', () => {
+ // Selecting a clinic with nothing persisted
+ let state = reducer(appliedFilters, {
+ type: 'SELECT_CLINIC_SUCCESS',
+ payload: { clinicId: 'clinic123', tideDashboardFilters: undefined },
+ });
+
+ expect(state).toStrictEqual(defaultFilters);
+
+ // Unsetting the clinic
+ state = reducer(appliedFilters, {
+ type: 'SELECT_CLINIC_SUCCESS',
+ payload: { clinicId: null },
+ });
+
+ expect(state).toStrictEqual(defaultFilters);
+ });
+ });
+});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.test.js
index 72e287c354..1866faa269 100644
--- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.test.js
@@ -9,12 +9,19 @@ import useDerivedDataRecencyEndpoints from '@app/pages/clinicworkspace/TideDashb
const mockStore = configureStore([thunk]);
describe('useDerivedDataRecencyEndpoints', () => {
- let store;
+ const renderEndpointsHook = (tideDashboardFilters = {}) => {
+ const store = mockStore({
+ blip: {
+ timePrefs: { timezoneAware: false, timezoneName: null },
+ tideDashboardFilters: tideDashboardFilters,
+ },
+ });
- const renderEndpointsHook = (filters) => renderHook(
- () => useDerivedDataRecencyEndpoints(filters),
- { wrapper: ({ children }) => {children} }
- ).result.current;
+ return renderHook(
+ () => useDerivedDataRecencyEndpoints(),
+ { wrapper: ({ children }) => {children} }
+ ).result.current;
+ };
beforeEach(() => {
// Fake only Date so "now" is pinned; the localized ceiling of now is 2025-05-30T00:00:00.000Z
@@ -29,8 +36,6 @@ describe('useDerivedDataRecencyEndpoints', () => {
'setTimeout', 'clearTimeout',
],
});
-
- store = mockStore({ blip: { timePrefs: { timezoneAware: false, timezoneName: null } } });
});
afterEach(() => {
@@ -71,11 +76,4 @@ describe('useDerivedDataRecencyEndpoints', () => {
'2025-05-30T00:00:00.000Z', // lastDataTo
]);
});
-
- it('defaults to the 7-day tide dashboard window when called without filters', () => {
- expect(renderEndpointsHook()).toStrictEqual([
- '2025-05-23T00:00:00.000Z', // lastDataFrom
- '2025-05-30T00:00:00.000Z', // lastDataTo
- ]);
- });
});
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters.test.js
new file mode 100644
index 0000000000..eaf20f6f0b
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters.test.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { renderHook } from '@testing-library/react-hooks/dom';
+import { thunk } from 'redux-thunk';
+import configureStore from 'redux-mock-store';
+
+import usePruneInvalidFilters from '@app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters';
+import { SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
+
+const mockStore = configureStore([thunk]);
+
+describe('usePruneInvalidFilters', () => {
+ let store;
+
+ const clinic = {
+ id: 'clinic123',
+ patientTags: [
+ { id: 'tag1', name: 'Week 1' },
+ { id: 'tag2', name: 'Week 2' },
+ // tag3 is deleted
+ ],
+ sites: [
+ { id: 'site1', name: 'Site Alpha' },
+ { id: 'site2', name: 'Site Bravo' },
+ // site3 is deleted
+ ],
+ };
+
+ const renderPruneHook = ({ filterOverrides = {}, clinics = { clinic123: clinic } } = {}) => {
+ store = mockStore({
+ blip: {
+ selectedClinicId: 'clinic123',
+ clinics,
+ tideDashboardFilters: {
+ lastData: 7,
+ patientTags: [],
+ clinicSites: [],
+ summaryPeriod: '14d',
+ ...filterOverrides,
+ },
+ },
+ });
+
+ renderHook(() => usePruneInvalidFilters(), {
+ wrapper: ({ children }) => {children},
+ });
+ };
+
+ it('prunes filtered tag ids that no longer exist in the clinic', () => {
+ renderPruneHook({ filterOverrides: { patientTags: ['tag1', 'tag3'] } });
+
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setPatientTagsFilter', payload: ['tag1'] },
+ ]);
+ });
+
+ it('prunes filtered site ids that no longer exist in the clinic', () => {
+ renderPruneHook({ filterOverrides: { clinicSites: ['site3', 'site2'] } });
+
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setClinicSitesFilter', payload: ['site2'] },
+ ]);
+ });
+
+ it('prunes invalid tags and sites in the same mount', () => {
+ renderPruneHook({
+ filterOverrides: {
+ patientTags: ['tag3', 'tag1'],
+ clinicSites: ['site3', 'site2'],
+ },
+ });
+
+ expect(store.getActions()).toStrictEqual([
+ { type: 'tideDashboardFilters/setPatientTagsFilter', payload: ['tag1'] },
+ { type: 'tideDashboardFilters/setClinicSitesFilter', payload: ['site2'] },
+ ]);
+ });
+
+ it('dispatches nothing when all filtered tags and sites still exist in the clinic', () => {
+ renderPruneHook({
+ filterOverrides: {
+ patientTags: ['tag1', 'tag2'],
+ clinicSites: ['site1', 'site2'],
+ },
+ });
+
+ expect(store.getActions()).toStrictEqual([]);
+ });
+
+ it('dispatches nothing when no tag or site filters are set', () => {
+ renderPruneHook();
+
+ expect(store.getActions()).toStrictEqual([]);
+ });
+
+ it('preserves the special zero-tags and zero-sites filter states', () => {
+ renderPruneHook({
+ filterOverrides: {
+ patientTags: SPECIAL_FILTER_STATES.ZERO_TAGS,
+ clinicSites: SPECIAL_FILTER_STATES.ZERO_SITES,
+ },
+ });
+
+ expect(store.getActions()).toStrictEqual([]);
+ });
+});
diff --git a/test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js b/__tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js
similarity index 100%
rename from test/unit/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js
rename to __tests__/unit/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.test.js
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/ActiveFiltersTray.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/ActiveFiltersTray.test.js
similarity index 75%
rename from __tests__/unit/app/pages/clinicworkspace/components/ActiveFiltersTray.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/ActiveFiltersTray.test.js
index f0e378517f..7abd548b98 100644
--- a/__tests__/unit/app/pages/clinicworkspace/components/ActiveFiltersTray.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/ActiveFiltersTray.test.js
@@ -8,18 +8,17 @@ 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';
+import ActiveFiltersTray from '@app/pages/clinicworkspace/components/filters/ActiveFiltersTray';
+import { defaultFilterState } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
const mockStore = configureStore([thunk]);
-const buildState = ({ fetchedPatientCount = 5 } = {}) => ({
+const buildState = () => ({
blip: {
selectedClinicId: 'clinic123',
clinics: {
'clinic123': {
id: 'clinic123',
- fetchedPatientCount,
patientTags: [
{ id: 'tag1', name: 'Tag One' },
{ id: 'tag2', name: 'Tag Two' },
@@ -35,37 +34,42 @@ const buildState = ({ fetchedPatientCount = 5 } = {}) => ({
const renderTray = ({
filters = defaultFilterState,
+ patientCount = 0,
hasSearchActive = false,
onRemoveFilter = jest.fn(),
state = buildState(),
} = {}) => {
const store = mockStore(state);
- const utils = render(
+ const ui = (props = {}) => (
);
- return { ...utils, onRemoveFilter };
+ const utils = render(ui());
+
+ return { ...utils, ui, onRemoveFilter };
};
describe('ActiveFiltersTray', () => {
describe('patient count header', () => {
- it('renders the fetched patient count', () => {
- renderTray({ state: buildState({ fetchedPatientCount: 5 }) });
+ it('renders the patient count', () => {
+ renderTray({ patientCount: 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 }) });
+ renderTray({ hasSearchActive: true, patientCount: 5 });
expect(screen.getByText('Showing 5 patients that match your search')).toBeInTheDocument();
});
@@ -110,6 +114,22 @@ describe('ActiveFiltersTray', () => {
});
});
+ describe('required filters', () => {
+ it('renders a required filter without its remove icon', () => {
+ const { ui, rerender } = renderTray({
+ filters: { ...defaultFilterState, lastData: 14, lastDataType: 'cgm' },
+ });
+
+ // Removable by default: the chip renders with its remove icon
+ expect(screen.getByLabelText('Remove CGM data within 14 days filter')).toBeInTheDocument();
+
+ // Marking the filter required keeps the chip but drops the remove icon
+ rerender(ui({ requiredFilters: { lastData: true } }));
+ expect(screen.getByText('CGM data within 14 days')).toBeInTheDocument();
+ expect(screen.queryByLabelText('Remove CGM data within 14 days filter')).not.toBeInTheDocument();
+ });
+ });
+
describe('removing a chip', () => {
it('fires onRemoveFilter with the chip type and value when its remove icon is clicked', async () => {
const { onRemoveFilter } = renderTray({
diff --git a/test/unit/pages/clinicworkspace/components/CGMUseFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/CGMUseFilterDropdown.test.js
similarity index 99%
rename from test/unit/pages/clinicworkspace/components/CGMUseFilterDropdown.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/CGMUseFilterDropdown.test.js
index dd66c5a5ed..cf43033293 100644
--- a/test/unit/pages/clinicworkspace/components/CGMUseFilterDropdown.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/CGMUseFilterDropdown.test.js
@@ -6,7 +6,7 @@ import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
-import CGMUseFilterDropdown from '@app/pages/clinicworkspace/components/CGMUseFilterDropdown';
+import CGMUseFilterDropdown from '@app/pages/clinicworkspace/components/filters/CGMUseFilterDropdown';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
const mockStore = configureStore([thunk]);
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown.test.js
similarity index 99%
rename from __tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown.test.js
index fa1aae7869..350e40d9ca 100644
--- a/__tests__/unit/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown.test.js
@@ -6,7 +6,7 @@ import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
-import DataRecencyFilterDropdown from '@app/pages/clinicworkspace/components/DataRecencyFilterDropdown';
+import DataRecencyFilterDropdown from '@app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
const mockStore = configureStore([thunk]);
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/SiteFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/SiteFilterDropdown.test.js
similarity index 99%
rename from __tests__/unit/app/pages/clinicworkspace/components/SiteFilterDropdown.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/SiteFilterDropdown.test.js
index a43191333b..4e651f367e 100644
--- a/__tests__/unit/app/pages/clinicworkspace/components/SiteFilterDropdown.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/SiteFilterDropdown.test.js
@@ -6,7 +6,7 @@ import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
-import SiteFilterDropdown from '@app/pages/clinicworkspace/components/SiteFilterDropdown';
+import SiteFilterDropdown from '@app/pages/clinicworkspace/components/filters/SiteFilterDropdown';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
import useIsClinicAdmin from '@app/pages/clinicworkspace/useIsClinicAdmin';
import { SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/SummaryPeriodFilterDropdown.test.js
similarity index 98%
rename from __tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/SummaryPeriodFilterDropdown.test.js
index 9c6946fa7b..a1ffcae930 100644
--- a/__tests__/unit/app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/SummaryPeriodFilterDropdown.test.js
@@ -6,7 +6,7 @@ import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
-import SummaryPeriodFilterDropdown from '@app/pages/clinicworkspace/components/SummaryPeriodFilterDropdown';
+import SummaryPeriodFilterDropdown from '@app/pages/clinicworkspace/components/filters/SummaryPeriodFilterDropdown';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
const mockStore = configureStore([thunk]);
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/TagFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/TagFilterDropdown.test.js
similarity index 99%
rename from __tests__/unit/app/pages/clinicworkspace/components/TagFilterDropdown.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/TagFilterDropdown.test.js
index 604e4c6224..3faf8ae63f 100644
--- a/__tests__/unit/app/pages/clinicworkspace/components/TagFilterDropdown.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/TagFilterDropdown.test.js
@@ -6,7 +6,7 @@ import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
-import TagFilterDropdown from '@app/pages/clinicworkspace/components/TagFilterDropdown';
+import TagFilterDropdown from '@app/pages/clinicworkspace/components/filters/TagFilterDropdown';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
import useIsClinicAdmin from '@app/pages/clinicworkspace/useIsClinicAdmin';
import { SPECIAL_FILTER_STATES } from '@app/pages/clinicworkspace/useClinicPatientsFilters';
diff --git a/__tests__/unit/app/pages/clinicworkspace/components/TimeInRangeFilterDropdown.test.js b/__tests__/unit/app/pages/clinicworkspace/components/filters/TimeInRangeFilterDropdown.test.js
similarity index 99%
rename from __tests__/unit/app/pages/clinicworkspace/components/TimeInRangeFilterDropdown.test.js
rename to __tests__/unit/app/pages/clinicworkspace/components/filters/TimeInRangeFilterDropdown.test.js
index 1c673bfa43..79ded58058 100644
--- a/__tests__/unit/app/pages/clinicworkspace/components/TimeInRangeFilterDropdown.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/components/filters/TimeInRangeFilterDropdown.test.js
@@ -7,7 +7,7 @@ import { thunk } from 'redux-thunk';
import { MemoryRouter } from 'react-router-dom';
import { useFlags } from 'launchdarkly-react-client-sdk';
-import TimeInRangeFilterDropdown from '@app/pages/clinicworkspace/components/TimeInRangeFilterDropdown';
+import TimeInRangeFilterDropdown from '@app/pages/clinicworkspace/components/filters/TimeInRangeFilterDropdown';
import { MMOLL_UNITS } from '@app/core/constants';
import { trackMetric as mockTrackMetric } from '../../../../../app/core/metricUtils';
diff --git a/__tests__/unit/app/pages/clinicworkspace/useClinicMetricsPageName.test.js b/__tests__/unit/app/pages/clinicworkspace/useClinicMetricsPageName.test.js
new file mode 100644
index 0000000000..45c9b7b564
--- /dev/null
+++ b/__tests__/unit/app/pages/clinicworkspace/useClinicMetricsPageName.test.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { renderHook } from '@testing-library/react-hooks/dom';
+
+import useClinicMetricsPageName from '@app/pages/clinicworkspace/useClinicMetricsPageName';
+
+describe('useClinicMetricsPageName', () => {
+ const renderPageNameHook = (route) => renderHook(
+ () => useClinicMetricsPageName(),
+ { wrapper: ({ children }) => {children} }
+ ).result.current;
+
+ it('returns Population Health arg', () => {
+ expect(renderPageNameHook('/clinic-workspace')).toBe('Population Health');
+ });
+
+ it('returns Population Health arg when backing out from patient data view', () => {
+ expect(renderPageNameHook('/clinic-workspace/patients')).toBe('Population Health');
+ });
+
+ it('returns TIDE Dashboard arg', () => {
+ expect(renderPageNameHook('/clinic-workspace/tide-dashboard')).toBe('TIDE Dashboard');
+ });
+});
diff --git a/__tests__/unit/app/redux/actions/async.test.js b/__tests__/unit/app/redux/actions/async.test.js
index d0159488a7..4c30ed7a71 100644
--- a/__tests__/unit/app/redux/actions/async.test.js
+++ b/__tests__/unit/app/redux/actions/async.test.js
@@ -2,28 +2,18 @@
/* global describe */
/* global it */
/* global expect */
-/* global beforeEach */
-/* global before */
/* global afterEach */
-/* global after */
-/* global context */
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
import trackingMiddleware from '../../../../../app/redux/utils/trackingMiddleware';
-import moment from 'moment';
-import _ from 'lodash';
-
-import isTSA from 'tidepool-standard-action';
import initialState from '../../../../../app/redux/reducers/initialState';
import * as ErrorMessages from '../../../../../app/redux/constants/errorMessages';
-import * as UserMessages from '../../../../../app/redux/constants/usrMessages';
// need to require() async in order to rewire utils inside
const async = require('../../../../../app/redux/actions/async');
-const sync = require('../../../../../app/redux/actions/sync');
describe('Actions', () => {
const trackMetric = jest.fn();
@@ -48,9 +38,6 @@ describe('Actions', () => {
{ type: 'CREATE_CLINIC_SITE_REQUEST' },
{ type: 'CREATE_CLINIC_SITE_SUCCESS', payload: { clinicId, site: newSite } },
];
- _.each(expectedActions, (action) => {
- expect(isTSA(action)).toBe(true);
- });
let store = mockStore({ blip: initialState });
store.dispatch(async.createClinicSite(api, clinicId, { name: 'Site Alpha' }));
@@ -76,9 +63,7 @@ describe('Actions', () => {
{ type: 'CREATE_CLINIC_SITE_REQUEST' },
{ type: 'CREATE_CLINIC_SITE_FAILURE', error: err, meta: { apiError: { status: 500, body: 'Error!' } } },
];
- _.each(expectedActions, (action) => {
- expect(isTSA(action)).toBe(true);
- });
+
let store = mockStore({ blip: initialState });
store.dispatch(async.createClinicSite(api, clinicId, { name: 'Site Charlie' }));
@@ -87,5 +72,173 @@ describe('Actions', () => {
expect(api.clinics.createClinicSite).toHaveBeenCalledTimes(1);
});
});
+
+ describe('selectClinic', () => {
+ const clinicId = 'clinic123';
+
+ const expectedUIDetails = {
+ entitlements: {
+ patientTags: false,
+ clinicSites: false,
+ prescriptions: false,
+ rpmReport: false,
+ summaryDashboard: false,
+ tideDashboard: false,
+ },
+ patientLimitEnforced: false,
+ planName: 'internationalBase',
+ ui: {
+ display: {
+ patientCount: true,
+ patientLimit: false,
+ planName: false,
+ workspacePlan: false,
+ workspaceLimitDescription: false,
+ workspaceLimitFeedback: false,
+ workspaceLimitResolutionLink: false,
+ },
+ text: {
+ planDisplayName: 'Base',
+ limitDescription: undefined,
+ limitFeedback: undefined,
+ limitResolutionLink: undefined,
+ },
+ warnings: {
+ limitApproaching: false,
+ limitReached: false,
+ },
+ },
+ };
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it('should trigger SELECT_CLINIC_SUCCESS, FETCH_CLINIC_PATIENT_COUNTS_SUCCESS, and FETCH_CLINIC_PATIENT_COUNT_SETTINGS_SUCCESS for a successful request', () => {
+ const countResults = { plan: 33 };
+ const settingsResults = { bar: 'baz' };
+
+ localStorage.setItem('tideDashboardFilters/user456/clinic123', JSON.stringify({ lastData: 7, patientTags: ['load1'] })); // this one should load
+ localStorage.setItem('tideDashboardFilters/wrongUser/clinic123', JSON.stringify({ lastData: 1 }));
+ localStorage.setItem('tideDashboardFilters/user456/wrongClinic', JSON.stringify({ lastData: 30 }));
+
+ let api = {
+ clinics: {
+ getClinicPatientCount: jest.fn().mockImplementation((_arg1, cb) => cb(null, countResults)),
+ getClinicPatientCountSettings: jest.fn().mockImplementation((_arg1, cb) => cb(null, settingsResults)),
+ },
+ };
+
+ let expectedActions = [
+ { type: 'SELECT_CLINIC_SUCCESS', payload: { clinicId, tideDashboardFilters: { lastData: 7, patientTags: ['load1'] } } }, // filters load from localState
+ { type: 'FETCH_CLINIC_PATIENT_COUNTS_REQUEST' },
+ { type: 'FETCH_CLINIC_PATIENT_COUNT_SETTINGS_REQUEST' },
+ { type: 'FETCH_CLINIC_PATIENT_COUNTS_SUCCESS', payload: { clinicId, patientCounts: countResults } },
+ { type: 'FETCH_CLINIC_PATIENT_COUNT_SETTINGS_SUCCESS', payload: { clinicId, patientCountSettings: settingsResults } },
+ { type: 'SET_CLINIC_UI_DETAILS', payload: { clinicId, uiDetails: expectedUIDetails } },
+ ];
+
+ let store = mockStore({ blip: {
+ ...initialState,
+ loggedInUserId: 'user456',
+ clinics: {
+ [clinicId]: {
+ patientCounts: undefined,
+ patientCountSettings: undefined,
+ },
+ },
+ } });
+
+ store.dispatch(async.selectClinic(api, clinicId));
+
+ const actions = store.getActions();
+ expect(actions).toStrictEqual(expectedActions);
+ expect(api.clinics.getClinicPatientCount).toHaveBeenCalledWith(clinicId, expect.any(Function));
+ expect(api.clinics.getClinicPatientCount).toHaveBeenCalledTimes(1);
+ expect(api.clinics.getClinicPatientCountSettings).toHaveBeenCalledWith(clinicId, expect.any(Function));
+ expect(api.clinics.getClinicPatientCountSettings).toHaveBeenCalledTimes(1);
+ });
+
+ it('should trigger SELECT_CLINIC_SUCCESS, but not FETCH_CLINIC_PATIENT_COUNTS_REQUEST or FETCH_CLINIC_PATIENT_COUNT_SETTINGS_REQUEST for a successful request if data available in clinic state', () => {
+ let api = {
+ clinics: {
+ getClinicPatientCount: jest.fn(),
+ getClinicPatientCountSettings: jest.fn(),
+ },
+ };
+
+ let expectedActions = [
+ { type: 'SELECT_CLINIC_SUCCESS', payload: { clinicId, tideDashboardFilters: undefined } },
+ { type: 'SET_CLINIC_UI_DETAILS', payload: { clinicId, uiDetails: expectedUIDetails } },
+ ];
+
+ let store = mockStore({ blip: {
+ ...initialState,
+ clinics: {
+ [clinicId]: {
+ patientCounts: { plan: 33 },
+ patientCountSettings: { foo: 'bar' },
+ },
+ },
+ } });
+
+ store.dispatch(async.selectClinic(api, clinicId));
+
+ const actions = store.getActions();
+ expect(actions).toStrictEqual(expectedActions);
+ expect(api.clinics.getClinicPatientCount).not.toHaveBeenCalled();
+ expect(api.clinics.getClinicPatientCountSettings).not.toHaveBeenCalled();
+ });
+
+ it('should trigger FETCH_CLINIC_PATIENT_COUNTS_FAILURE and FETCH_CLINIC_PATIENT_COUNT_SETTINGS_FAILURE and it should call error once for a failed request', () => {
+ let api = {
+ clinics: {
+ getClinicPatientCount: jest.fn().mockImplementation((_arg1, cb) => cb({ status: 500, body: 'Count Error!' }, null)),
+ getClinicPatientCountSettings: jest.fn().mockImplementation((_arg1, cb) => cb({ status: 500, body: 'Settings Error!' }, null)),
+ },
+ };
+
+ let countErr = new Error(ErrorMessages.ERR_FETCHING_CLINIC_PATIENT_COUNTS);
+ countErr.status = 500;
+
+ let settingsErr = new Error(ErrorMessages.ERR_FETCHING_CLINIC_PATIENT_COUNT_SETTINGS);
+ settingsErr.status = 500;
+
+ let expectedActions = [
+ { type: 'SELECT_CLINIC_SUCCESS', payload: { clinicId, tideDashboardFilters: undefined } },
+ { type: 'FETCH_CLINIC_PATIENT_COUNTS_REQUEST' },
+ { type: 'FETCH_CLINIC_PATIENT_COUNT_SETTINGS_REQUEST' },
+ {
+ type: 'FETCH_CLINIC_PATIENT_COUNTS_FAILURE',
+ error: countErr,
+ meta: { apiError: { status: 500, body: 'Count Error!' } },
+ },
+ {
+ type: 'FETCH_CLINIC_PATIENT_COUNT_SETTINGS_FAILURE',
+ error: settingsErr,
+ meta: { apiError: { status: 500, body: 'Settings Error!' } },
+ },
+ ];
+
+ let store = mockStore({ blip: {
+ ...initialState,
+ clinics: {
+ [clinicId]: {
+ patientCounts: undefined,
+ patientCountSettings: undefined,
+ },
+ },
+ } });
+
+ store.dispatch(async.selectClinic(api, clinicId));
+
+ const actions = store.getActions();
+ expect(actions).toStrictEqual(expectedActions);
+ expect(api.clinics.getClinicPatientCount).toHaveBeenCalledWith(clinicId, expect.any(Function));
+ expect(api.clinics.getClinicPatientCount).toHaveBeenCalledTimes(1);
+ expect(api.clinics.getClinicPatientCountSettings).toHaveBeenCalledWith(clinicId, expect.any(Function));
+ expect(api.clinics.getClinicPatientCountSettings).toHaveBeenCalledTimes(1);
+ });
+ });
});
});
diff --git a/__tests__/unit/app/redux/store/configureStore.dev.test.js b/__tests__/unit/app/redux/store/configureStore.dev.test.js
new file mode 100644
index 0000000000..b3f282fcaf
--- /dev/null
+++ b/__tests__/unit/app/redux/store/configureStore.dev.test.js
@@ -0,0 +1,49 @@
+/* global jest */
+/* global describe */
+/* global it */
+/* global expect */
+/* global beforeEach */
+/* global afterEach */
+
+import configureStore from '@app/redux/store/configureStore.dev';
+import { setTideDashboardFilters } from '@app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice';
+
+describe('configureStore.dev', () => {
+ const api = {
+ metrics: { track: jest.fn() },
+ errors: { log: jest.fn() },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ localStorage.clear();
+ });
+
+ it('should persist select items to localStorage, scoped to the logged-in user and selected clinic', () => {
+ // Log in and select a clinic
+ const store = configureStore(api);
+ store.dispatch({ type: 'FETCH_USER_SUCCESS', payload: { user: { userid: 'user456' } } });
+ store.dispatch({ type: 'SELECT_CLINIC_SUCCESS', payload: { clinicId: 'clinic123' } });
+ store.dispatch(setTideDashboardFilters({ patientTags: ['tag1'], summaryPeriod: '7d' }));
+
+ jest.advanceTimersByTime(1200); // subscriber is throttled to 1 second
+
+ expect(JSON.parse(localStorage.getItem('blipState'))).toStrictEqual({ selectedClinicId: 'clinic123' });
+ expect(JSON.parse(localStorage.getItem('tideDashboardFilters/user456/clinic123'))).toStrictEqual({ patientTags: ['tag1'], summaryPeriod: '7d' });
+ });
+
+ it('should not persist items no clinic is selected', () => {
+ const store = configureStore(api);
+ store.dispatch({ type: 'FETCH_USER_SUCCESS', payload: { user: { userid: 'user456' } } });
+
+ jest.advanceTimersByTime(1200); // subscriber is throttled to 1 second
+
+ expect(JSON.parse(localStorage.getItem('blipState'))).toStrictEqual({ selectedClinicId: null });
+ expect(JSON.parse(localStorage.getItem('tideDashboardFilters/user456/clinic123'))).toStrictEqual(null);
+ });
+});
diff --git a/__tests__/unit/app/redux/store/configureStore.prod.test.js b/__tests__/unit/app/redux/store/configureStore.prod.test.js
new file mode 100644
index 0000000000..7fbc0adac2
--- /dev/null
+++ b/__tests__/unit/app/redux/store/configureStore.prod.test.js
@@ -0,0 +1,49 @@
+/* global jest */
+/* global describe */
+/* global it */
+/* global expect */
+/* global beforeEach */
+/* global afterEach */
+
+import configureStore from '@app/redux/store/configureStore.prod';
+import { setTideDashboardFilters } from '@app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice';
+
+describe('configureStore.prod', () => {
+ const api = {
+ metrics: { track: jest.fn() },
+ errors: { log: jest.fn() },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ localStorage.clear();
+ });
+
+ it('should persist select items to localStorage, scoped to the logged-in user and selected clinic', () => {
+ // Log in and select a clinic
+ const store = configureStore(api);
+ store.dispatch({ type: 'FETCH_USER_SUCCESS', payload: { user: { userid: 'user456' } } });
+ store.dispatch({ type: 'SELECT_CLINIC_SUCCESS', payload: { clinicId: 'clinic123' } });
+ store.dispatch(setTideDashboardFilters({ patientTags: ['tag1'], summaryPeriod: '7d' }));
+
+ jest.advanceTimersByTime(1200); // subscriber is throttled to 1 second
+
+ expect(JSON.parse(localStorage.getItem('blipState'))).toStrictEqual({ selectedClinicId: 'clinic123' });
+ expect(JSON.parse(localStorage.getItem('tideDashboardFilters/user456/clinic123'))).toStrictEqual({ patientTags: ['tag1'], summaryPeriod: '7d' });
+ });
+
+ it('should not persist items no clinic is selected', () => {
+ const store = configureStore(api);
+ store.dispatch({ type: 'FETCH_USER_SUCCESS', payload: { user: { userid: 'user456' } } });
+
+ jest.advanceTimersByTime(1200); // subscriber is throttled to 1 second
+
+ expect(JSON.parse(localStorage.getItem('blipState'))).toStrictEqual({ selectedClinicId: null });
+ expect(JSON.parse(localStorage.getItem('tideDashboardFilters/user456/clinic123'))).toStrictEqual(null);
+ });
+});
diff --git a/app/pages/clinicworkspace/TideDashboardV2/EmptyContentNode.js b/app/pages/clinicworkspace/TideDashboardV2/EmptyContentNode.js
index 33c346efad..a29a9332bf 100644
--- a/app/pages/clinicworkspace/TideDashboardV2/EmptyContentNode.js
+++ b/app/pages/clinicworkspace/TideDashboardV2/EmptyContentNode.js
@@ -1,15 +1,40 @@
import React from 'react';
+import { useSelector, useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import colorPalette from '../../../themes/colorPalette';
import { Flex, Text } from 'theme-ui';
+import { resetTideDashboardFilters } from './tideDashboardFiltersSlice';
+import { setOffset } from './tideDashboardSlice';
+import ClearFilterButtons, { PATIENT_QUERY_STATE } from '../components/ClearFilterButtons';
+import noop from 'lodash/noop';
+import without from 'lodash/without';
const EmptyContentNode = () => {
const { t } = useTranslation();
+ const dispatch = useDispatch();
- const emptyContentCopy = t('There are no results to show');
+ const { patientTags, clinicSites } = useSelector(state => state.blip.tideDashboardFilters);
+
+ const activeFiltersCount = without([
+ patientTags?.length,
+ clinicSites?.length,
+ ], null, 0, undefined).length;
+
+ const hasActiveFilters = activeFiltersCount > 0;
+
+ const handleResetFilters = () => {
+ dispatch(resetTideDashboardFilters());
+ dispatch(setOffset(0));
+ };
+
+ const emptyContentCopy = hasActiveFilters
+ ? t('There are no patients with the current filter(s)')
+ : t('There are no results to show');
+
+ const patientQueryState = hasActiveFilters ? PATIENT_QUERY_STATE.FILTER_ONLY : PATIENT_QUERY_STATE.NONE;
return (
- {
{emptyContentCopy}
+
+ { hasActiveFilters &&
+
+ }
);
};
diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js
index 445203ca88..1d431c0e5d 100644
--- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js
+++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js
@@ -1,23 +1,36 @@
import React, { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import Table from '../../../components/elements/Table';
-import { Flex } from 'theme-ui';
+import { Flex, Text, Box } from 'theme-ui';
-import FilterByCategory from './FilterByCategory';
+import FilterByCategory from './filters/FilterByCategory';
+import FilterByTags from './filters/FilterByTags';
+import FilterBySites from './filters/FilterBySites';
+import FilterByDataRecency from './filters/FilterByDataRecency';
+import FilterBySummaryPeriod from './filters/FilterBySummaryPeriod';
+import AppliedFiltersList from './filters/AppliedFiltersList';
import TableCategoryHeader from './TableCategoryHeader';
import PaginationController from './PaginationController';
import useTideDashboardPatients from './useTideDashboardPatients';
+import usePruneInvalidFilters from './usePruneInvalidFilters';
import useTableColumns from './useTableColumns';
import EmptyContentNode from './EmptyContentNode';
import { Redirect, useLocation } from 'react-router-dom';
import useAuthorizationGate from './useAuthorizationGate';
+const Gap = () => ;
+
const tableContainerProps = { sx: { containerType: 'inline-size' } };
const TideDashboardV2 = () => {
const { search } = useLocation();
+ const { t } = useTranslation();
+
+ usePruneInvalidFilters();
+
const category = useSelector(state => state.blip.tideDashboard.category);
const { isAuthorized, isUnauthorized } = useAuthorizationGate();
@@ -41,11 +54,21 @@ const TideDashboardV2 = () => {
return (
<>
+
+ {t('Filter By')}
+
+
+
+
+
+
+
+
{
+ const hasFiltersActive = clinicSites?.length > 0 || patientTags?.length > 0;
+
+ if (hasFiltersActive) return PATIENT_QUERY_STATE.FILTER_ONLY;
+
+ return PATIENT_QUERY_STATE.NONE;
+};
+
+const AppliedFiltersList = ({ patientCount = 0 }) => {
+ const dispatch = useDispatch();
+
+ const { lastData, clinicSites, patientTags } = useSelector(state => state.blip.tideDashboardFilters);
+
+ // Last Data is a required filter to be applied
+ const requiredFilters = { lastData: true };
+ const activeFilters = { lastDataType: 'cgm', lastData, clinicSites, patientTags };
+
+ const handleResetFilters = () => {
+ dispatch(setPatientTagsFilter([]));
+ dispatch(setClinicSitesFilter([]));
+ dispatch(setOffset(0));
+ };
+
+ const handleRemoveFilter = (filterKey, value) => {
+ switch (filterKey) {
+ case 'patientTags': {
+ const updatedTags = without(patientTags, value);
+ dispatch(setPatientTagsFilter(updatedTags));
+ dispatch(setOffset(0));
+ break;
+ }
+
+ case 'clinicSites': {
+ const updatedSites = without(clinicSites, value);
+ dispatch(setClinicSitesFilter(updatedSites));
+ dispatch(setOffset(0));
+ break;
+ }
+ }
+ };
+
+ const patientQueryState = getPatientQueryState(patientTags, clinicSites);
+
+ return (
+
+
+
+ }
+ />
+ );
+};
+
+AppliedFiltersList.propTypes = {
+ patientCount: PropTypes.number,
+};
+
+export default AppliedFiltersList;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/FilterByCategory.js b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByCategory.js
similarity index 93%
rename from app/pages/clinicworkspace/TideDashboardV2/FilterByCategory.js
rename to app/pages/clinicworkspace/TideDashboardV2/filters/FilterByCategory.js
index d1f68697d4..bb8b8e0850 100644
--- a/app/pages/clinicworkspace/TideDashboardV2/FilterByCategory.js
+++ b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByCategory.js
@@ -1,8 +1,8 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector, useDispatch } from 'react-redux';
-import { SegmentedControl, Segment } from '../components/SegmentedControl';
-import { setCategory, setOffset, CATEGORY } from './tideDashboardSlice';
+import { SegmentedControl, Segment } from '../../components/SegmentedControl';
+import { setCategory, setOffset, CATEGORY } from '../tideDashboardSlice';
import { colors as vizColors } from '@tidepool/viz';
import { Box } from 'theme-ui';
diff --git a/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency.js b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency.js
new file mode 100644
index 0000000000..ac72ffde5b
--- /dev/null
+++ b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByDataRecency.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setOffset } from '../tideDashboardSlice';
+import { setLastDataFilter } from '../tideDashboardFiltersSlice';
+
+import { lastDataFilterOptions } from '../../../../core/clinicUtils';
+
+import DataRecencyFilterDropdown from '../../components/filters/DataRecencyFilterDropdown';
+
+const FilterByDataRecency = () => {
+ const dispatch = useDispatch();
+ const { lastData } = useSelector(state => state.blip.tideDashboardFilters);
+
+ const handleChange = ({ lastData }) => {
+ dispatch(setLastDataFilter(lastData));
+ dispatch(setOffset(0));
+ };
+
+ const customLastDataFilterOptions = lastDataFilterOptions.filter(opt => [1, 2, 7, 14].includes(opt.value));
+
+ return (
+
+ );
+};
+
+export default FilterByDataRecency;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites.js b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites.js
new file mode 100644
index 0000000000..1d33c9f33f
--- /dev/null
+++ b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySites.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setOffset } from '../tideDashboardSlice';
+import { setClinicSitesFilter } from '../tideDashboardFiltersSlice';
+import SiteFilterDropdown from '../../components/filters/SiteFilterDropdown';
+
+const FilterBySites = () => {
+ const dispatch = useDispatch();
+ const { clinicSites } = useSelector(state => state.blip.tideDashboardFilters);
+
+ const handleChange = (clinicSites) => {
+ dispatch(setClinicSitesFilter(clinicSites));
+ dispatch(setOffset(0));
+ };
+
+ return ;
+};
+
+export default FilterBySites;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod.js b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod.js
new file mode 100644
index 0000000000..9fb359cabd
--- /dev/null
+++ b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterBySummaryPeriod.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import SummaryPeriodFilterDropdown from '../../components/filters/SummaryPeriodFilterDropdown';
+import { setSummaryPeriodFilter } from '../tideDashboardFiltersSlice';
+import { setOffset } from '../tideDashboardSlice';
+
+const FilterBySummaryPeriod = () => {
+ const dispatch = useDispatch();
+ const { summaryPeriod } = useSelector(state => state.blip.tideDashboardFilters);
+
+ const handleChange = (summaryPeriod) => {
+ dispatch(setSummaryPeriodFilter(summaryPeriod));
+ dispatch(setOffset(0));
+ };
+
+ return (
+
+ );
+};
+
+export default FilterBySummaryPeriod;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags.js b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags.js
new file mode 100644
index 0000000000..ce9c9c7321
--- /dev/null
+++ b/app/pages/clinicworkspace/TideDashboardV2/filters/FilterByTags.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setOffset } from '../tideDashboardSlice';
+import { setPatientTagsFilter } from '../tideDashboardFiltersSlice';
+import TagFilterDropdown from '../../components/filters/TagFilterDropdown';
+
+const FilterByTags = () => {
+ const dispatch = useDispatch();
+ const { patientTags } = useSelector(state => state.blip.tideDashboardFilters);
+
+ const handleChange = (tags) => {
+ dispatch(setPatientTagsFilter(tags));
+ dispatch(setOffset(0));
+ };
+
+ return ;
+};
+
+export default FilterByTags;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice.js b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice.js
new file mode 100644
index 0000000000..e24dae61e2
--- /dev/null
+++ b/app/pages/clinicworkspace/TideDashboardV2/tideDashboardFiltersSlice.js
@@ -0,0 +1,46 @@
+import { createSlice } from '@reduxjs/toolkit';
+import { SELECT_CLINIC_SUCCESS } from '../../../redux/constants/actionTypes';
+
+const getInitialState = () => ({
+ lastData: 7,
+ patientTags: [],
+ clinicSites: [],
+ summaryPeriod: '14d',
+});
+
+const tideDashboardFiltersSlice = createSlice({
+ name: 'tideDashboardFilters',
+ initialState: getInitialState(),
+ reducers: {
+ setTideDashboardFilters: (_state, action) => action.payload || getInitialState(),
+ setLastDataFilter: (state, action) => {
+ state.lastData = action.payload;
+ },
+ setPatientTagsFilter: (state, action) => {
+ state.patientTags = action.payload;
+ },
+ setClinicSitesFilter: (state, action) => {
+ state.clinicSites = action.payload;
+ },
+ setSummaryPeriodFilter: (state, action) => {
+ state.summaryPeriod = action.payload;
+ },
+ resetTideDashboardFilters: () => getInitialState(),
+ },
+ extraReducers: (builder) => {
+ builder.addCase(SELECT_CLINIC_SUCCESS, (_state, action) => {
+ return action.payload?.tideDashboardFilters || getInitialState();
+ });
+ },
+});
+
+export const {
+ setTideDashboardFilters,
+ setLastDataFilter,
+ setPatientTagsFilter,
+ setClinicSitesFilter,
+ setSummaryPeriodFilter,
+ resetTideDashboardFilters,
+} = tideDashboardFiltersSlice.actions;
+
+export default tideDashboardFiltersSlice.reducer;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.js b/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.js
index 10693ffede..edb7c24656 100644
--- a/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.js
+++ b/app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints.js
@@ -4,18 +4,8 @@ import { useSelector } from 'react-redux';
import { utils as vizUtils } from '@tidepool/viz';
const { getLocalizedCeiling } = vizUtils.datetime;
-// TEMPORARY, will be set in redux
-const tideDashboardFilters = {
- lastData: 7,
- patientTags: [],
- clinicSites: [],
- summaryPeriod: '14d',
-};
-
-const useDerivedDataRecencyEndpoints = (
- filters = tideDashboardFilters // TEMPORARY, will be set in redux
-) => {
- const lastData = filters.lastData;
+const useDerivedDataRecencyEndpoints = () => {
+ const lastData = useSelector(state => state.blip.tideDashboardFilters.lastData);
const timePrefs = useSelector((state) => state.blip.timePrefs);
const lastDataTo = getLocalizedCeiling(new Date().toISOString(), timePrefs).toISOString();
diff --git a/app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters.js b/app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters.js
new file mode 100644
index 0000000000..1713447831
--- /dev/null
+++ b/app/pages/clinicworkspace/TideDashboardV2/usePruneInvalidFilters.js
@@ -0,0 +1,43 @@
+import { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { setPatientTagsFilter, setClinicSitesFilter } from './tideDashboardFiltersSlice';
+import { SPECIAL_FILTER_STATES } from '../useClinicPatientsFilters';
+import keyBy from 'lodash/keyBy';
+import isEqual from 'lodash/isEqual';
+
+const usePruneInvalidFilters = () => {
+ const dispatch = useDispatch();
+ const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
+ const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
+ const { patientTags, clinicSites } = useSelector(state => state.blip.tideDashboardFilters);
+
+ const clinicId = clinic?.id;
+
+ useEffect(() => {
+ if (!patientTags?.length || !clinicId) return;
+
+ if (isEqual(patientTags, SPECIAL_FILTER_STATES.ZERO_TAGS)) return;
+
+ const availableTags = keyBy(clinic.patientTags || [], 'id');
+ const prunedTags = patientTags.filter(tagId => !!availableTags[tagId]);
+
+ if (prunedTags.length < patientTags.length) {
+ dispatch(setPatientTagsFilter(prunedTags));
+ }
+ }, [clinicId]);
+
+ useEffect(() => {
+ if (!clinicSites?.length || !clinicId) return;
+
+ if (isEqual(clinicSites, SPECIAL_FILTER_STATES.ZERO_SITES)) return;
+
+ const availableSites = keyBy(clinic.sites || [], 'id');
+ const prunedSites = clinicSites.filter(siteId => !!availableSites[siteId]);
+
+ if (prunedSites.length < clinicSites.length) {
+ dispatch(setClinicSitesFilter(prunedSites));
+ }
+ }, [clinicId]);
+};
+
+export default usePruneInvalidFilters;
diff --git a/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js b/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js
index 63a10a9adb..c6454a6526 100644
--- a/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js
+++ b/app/pages/clinicworkspace/TideDashboardV2/useTideDashboardPatients.js
@@ -4,21 +4,13 @@ import useDerivedDataRecencyEndpoints from './useDerivedDataRecencyEndpoints';
const LIMIT = 12;
-// TEMPORARY, will be set in redux
-const tideDashboardFilters = {
- lastData: 7,
- patientTags: [],
- clinicSites: [],
- summaryPeriod: '14d',
-};
-
const useTideDashboardPatients = () => {
const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const category = useSelector(state => state.blip.tideDashboard.category);
const offset = useSelector(state => state.blip.tideDashboard.offset);
- const patientTags = tideDashboardFilters.patientTags;
- const clinicSites = tideDashboardFilters.clinicSites;
- const summaryPeriod = tideDashboardFilters.summaryPeriod;
+ const patientTags = useSelector(state => state.blip.tideDashboardFilters.patientTags);
+ const clinicSites = useSelector(state => state.blip.tideDashboardFilters.clinicSites);
+ const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod);
const [lastDataFrom, lastDataTo] = useDerivedDataRecencyEndpoints();
return useGetTideDashboardPatientsQuery(
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.js b/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.js
index 3bcb865899..545e016888 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/AppliedFiltersList.js
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import without from 'lodash/without';
-import ActiveFiltersTray from '../components/ActiveFiltersTray';
+import ActiveFiltersTray from '../components/filters/ActiveFiltersTray';
import ClearFilterButtons, { PATIENT_QUERY_STATE } from '../components/ClearFilterButtons';
import { defaultFilterState } from '../useClinicPatientsFilters';
import { Box } from 'theme-ui';
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.js
index 1c6f76bc25..71d7ef6dce 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByCGMUse.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import noop from 'lodash/noop';
-import CGMUseFilterDropdown from '../components/CGMUseFilterDropdown';
+import CGMUseFilterDropdown from '../components/filters/CGMUseFilterDropdown';
const FilterByCGMUse = ({
activeFilters = {},
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js
index b48b4036bf..8e3d62161e 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByDataRecency.js
@@ -6,7 +6,7 @@ import reject from 'lodash/reject';
import { lastDataFilterOptions } from '../../../core/clinicUtils';
-import DataRecencyFilterDropdown from '../components/DataRecencyFilterDropdown';
+import DataRecencyFilterDropdown from '../components/filters/DataRecencyFilterDropdown';
const FilterByDataRecency = ({
activeFilters = {},
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.js
index c0990ce846..bec3afbda5 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySites.js
@@ -5,7 +5,7 @@ import * as actions from '../../../redux/actions';
import { trackMetric } from '../../../core/metricUtils';
import noop from 'lodash/noop';
-import SiteFilterDropdown from '../components/SiteFilterDropdown';
+import SiteFilterDropdown from '../components/filters/SiteFilterDropdown';
import useIsClinicAdmin from '../useIsClinicAdmin';
import useClinicMetricsPageName from '../useClinicMetricsPageName';
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.js
index 9e759560a5..e98b5f67d3 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterBySummaryPeriod.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import noop from 'lodash/noop';
-import SummaryPeriodFilterDropdown from '../components/SummaryPeriodFilterDropdown';
+import SummaryPeriodFilterDropdown from '../components/filters/SummaryPeriodFilterDropdown';
import { summaryPeriodOptions } from '../../../core/clinicUtils';
const FilterBySummaryPeriod = ({
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.js
index 997d1ddf0c..6b0a48c090 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTags.js
@@ -5,7 +5,7 @@ import * as actions from '../../../redux/actions';
import { trackMetric } from '../../../core/metricUtils';
import noop from 'lodash/noop';
-import TagFilterDropdown from '../components/TagFilterDropdown';
+import TagFilterDropdown from '../components/filters/TagFilterDropdown';
import useIsClinicAdmin from '../useIsClinicAdmin';
import useClinicMetricsPageName from '../useClinicMetricsPageName';
diff --git a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.js b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.js
index 087dfabf00..36ca835686 100644
--- a/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.js
+++ b/app/pages/clinicworkspace/clinicPatientsFilters/FilterByTimeInRange.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import noop from 'lodash/noop';
-import TimeInRangeFilterDropdown from '../components/TimeInRangeFilterDropdown';
+import TimeInRangeFilterDropdown from '../components/filters/TimeInRangeFilterDropdown';
const FilterByTimeInRange = ({
activeFilters = {},
diff --git a/app/pages/clinicworkspace/components/ActiveFiltersTray.js b/app/pages/clinicworkspace/components/filters/ActiveFiltersTray.js
similarity index 86%
rename from app/pages/clinicworkspace/components/ActiveFiltersTray.js
rename to app/pages/clinicworkspace/components/filters/ActiveFiltersTray.js
index d8e6596fa3..20ae9bde08 100644
--- a/app/pages/clinicworkspace/components/ActiveFiltersTray.js
+++ b/app/pages/clinicworkspace/components/filters/ActiveFiltersTray.js
@@ -1,24 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
-import { useTranslation, withTranslation } from 'react-i18next';
-import { Flex, Text, Box } from 'theme-ui';
+import { useTranslation } from 'react-i18next';
+import { Flex, Text } from 'theme-ui';
import { colors as vizColors } from '@tidepool/viz';
import CloseRoundedIcon from '@material-ui/icons/CloseRounded';
import LocationOnOutlinedIcon from '@material-ui/icons/LocationOnOutlined';
-import TagIcon from '../../../core/icons/tagIcon.svg';
+import TagIcon from '../../../../core/icons/tagIcon.svg';
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
import noop from 'lodash/noop';
-import Icon from '../../../components/elements/Icon';
-import utils from '../../../core/utils';
-import { transitions } from '../../../themes/baseTheme';
-import { SPECIAL_FILTER_STATES } from '../useClinicPatientsFilters';
+import Icon from '../../../../components/elements/Icon';
+import utils from '../../../../core/utils';
+import { transitions } from '../../../../themes/baseTheme';
+import { SPECIAL_FILTER_STATES } from '../../useClinicPatientsFilters';
-const usePrimaryChips = (activeFilters) => {
+const usePrimaryChips = (activeFilters, requiredFilters) => {
const { t } = useTranslation();
const { lastData, lastDataType, timeCGMUsePercent, timeInRange = [] } = activeFilters;
@@ -47,6 +47,7 @@ const usePrimaryChips = (activeFilters) => {
type: 'lastData',
value: `${lastDataType}-${lastData}`,
label: getLastDataChipLabel(lastDataType, lastData),
+ required: requiredFilters?.['lastData'] || false,
}),
// CGM Wear Time Filter
@@ -111,8 +112,9 @@ const useSiteChips = (clinicSites = []) => {
.toSorted((a, b) => utils.compareLabels(a.label, b.label));
};
-const Chip = ({ label, onRemove }) => {
+const Chip = ({ label, onRemove, required = false }) => {
const { t } = useTranslation();
+ const canRemove = !required;
return (
{
fontWeight: 'normal',
cursor: 'default',
ml: 1,
- '&:hover': {
+ '&:hover': canRemove ? {
color: vizColors.blue80,
fontWeight: 'medium',
- },
+ } : {},
'.remove-filter-icon': {
fontSize: '14px',
padding: '2px',
@@ -150,16 +152,18 @@ const Chip = ({ label, onRemove }) => {
},
}}
>
-
+
{label}
-
+ { canRemove &&
+
+ }
);
};
@@ -175,6 +179,7 @@ const ChipGroup = ({ prefix, chips, onRemove }) => {
onRemove(chip)}
/>
))}
@@ -183,23 +188,22 @@ const ChipGroup = ({ prefix, chips, onRemove }) => {
};
const ActiveFiltersTray = ({
+ patientCount = 0,
filters = {},
+ requiredFilters = {},
hasSearchActive = false,
onRemoveFilter = noop,
rightContent = null,
}) => {
const { t } = useTranslation();
- const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
- const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
-
- const primaryChips = usePrimaryChips(filters);
+ const primaryChips = usePrimaryChips(filters, requiredFilters);
const tagChips = useTagChips(filters.patientTags);
const siteChips = useSiteChips(filters.clinicSites);
- const count = clinic?.fetchedPatientCount || 0;
-
const handleRemoveChip = chip => onRemoveFilter(chip.type, chip.value);
+ const count = patientCount;
+
return (
[
{ value: '<0.7', label: t('Less than 70%') },
diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown.js
similarity index 71%
rename from app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js
rename to app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown.js
index a1c306ee61..3d0d85d566 100644
--- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js
+++ b/app/pages/clinicworkspace/components/filters/DataRecencyFilterDropdown.js
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
-import { trackMetric } from '../../../core/metricUtils';
+import { trackMetric } from '../../../../core/metricUtils';
import { colors as vizColors } from '@tidepool/viz';
import { Box, Grid } from 'theme-ui';
@@ -11,13 +11,15 @@ 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 { lastDataFilterOptions } from '../../../core/clinicUtils';
-import useClinicMetricsPageName from '../useClinicMetricsPageName';
+import Button from '../../../../components/elements/Button';
+import Popover from '../../../../components/elements/Popover';
+import RadioGroup from '../../../../components/elements/RadioGroup';
+import { lastDataFilterOptions } from '../../../../core/clinicUtils';
+import useClinicMetricsPageName from '../../useClinicMetricsPageName';
const DropdownContent = ({
+ canSelectLastDataType,
+ canClearSelection,
onClose,
onChange,
lastData,
@@ -40,25 +42,29 @@ const DropdownContent = ({
return (
-
-
- {t('Device Type')}
-
-
-
-
- {
- setPending({ ...pending, lastDataType: event.target.value || null });
- }}
- />
-
+ {canSelectLastDataType &&
+ <>
+
+
+ {t('Device Type')}
+
+
+
+
+ {
+ setPending({ ...pending, lastDataType: event.target.value || null });
+ }}
+ />
+
+ >
+ }
{t('Data Recency')}
@@ -82,17 +88,20 @@ const DropdownContent = ({