From 36223c1574823ab3df5402c75a875908d7c6e68e Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 17:01:37 -0700 Subject: [PATCH 001/123] WEB-4460 initialize base branch From 7efc2ae2b25b1e11b76d4c7924e674aa849d5452 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 15:32:42 -0700 Subject: [PATCH 002/123] WEB-4460 add new adapter components to dashboard --- .../TideDashboardV2/AppliedFiltersList.js | 81 +++++++++++++++++++ .../TideDashboardV2/FilterByDataRecency.js | 33 ++++++++ .../TideDashboardV2/FilterBySites.js | 19 +++++ .../TideDashboardV2/FilterBySummaryPeriod.js | 25 ++++++ .../TideDashboardV2/FilterByTags.js | 19 +++++ .../TideDashboardV2/TideDashboardV2.js | 19 ++++- .../components/ActiveFiltersTray.js | 37 +++++---- 7 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/AppliedFiltersList.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/FilterByDataRecency.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/FilterBySites.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/FilterBySummaryPeriod.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/FilterByTags.js diff --git a/app/pages/clinicworkspace/TideDashboardV2/AppliedFiltersList.js b/app/pages/clinicworkspace/TideDashboardV2/AppliedFiltersList.js new file mode 100644 index 0000000000..3709028afc --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/AppliedFiltersList.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import without from 'lodash/without'; +import noop from 'lodash/noop'; + +import ActiveFiltersTray from '../components/ActiveFiltersTray'; +import ClearFilterButtons, { PATIENT_QUERY_STATE } from '../components/ClearFilterButtons'; +import { Box } from 'theme-ui'; +import { setClinicSitesFilter, setPatientTagsFilter } from './tideDashboardFiltersSlice'; +import { setOffset } from './tideDashboardSlice'; + +export const getPatientQueryState = (patientTags, clinicSites) => { + 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); + + 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/FilterByDataRecency.js b/app/pages/clinicworkspace/TideDashboardV2/FilterByDataRecency.js new file mode 100644 index 0000000000..843092e8e7 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/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/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].includes(opt.value)); + + return ( + + ); +}; + +export default FilterByDataRecency; diff --git a/app/pages/clinicworkspace/TideDashboardV2/FilterBySites.js b/app/pages/clinicworkspace/TideDashboardV2/FilterBySites.js new file mode 100644 index 0000000000..8849379403 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/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/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/FilterBySummaryPeriod.js b/app/pages/clinicworkspace/TideDashboardV2/FilterBySummaryPeriod.js new file mode 100644 index 0000000000..569a1449c1 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/FilterBySummaryPeriod.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import SummaryPeriodFilterDropdown from '../components/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/FilterByTags.js b/app/pages/clinicworkspace/TideDashboardV2/FilterByTags.js new file mode 100644 index 0000000000..eeb5f84f79 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/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/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/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index 445203ca88..ca6cd56075 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -1,9 +1,12 @@ import React, { useMemo } from 'react'; 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 FilterByTags from './FilterByTags'; +import FilterByDataRecency from './FilterByDataRecency'; +import FilterBySummaryPeriod from './FilterBySummaryPeriod'; import TableCategoryHeader from './TableCategoryHeader'; import PaginationController from './PaginationController'; @@ -13,6 +16,10 @@ import useTableColumns from './useTableColumns'; import EmptyContentNode from './EmptyContentNode'; import { Redirect, useLocation } from 'react-router-dom'; import useAuthorizationGate from './useAuthorizationGate'; +import FilterBySites from './FilterBySites'; +import AppliedFiltersList from './AppliedFiltersList'; + +const Gap = () => ; const tableContainerProps = { sx: { containerType: 'inline-size' } }; @@ -41,11 +48,21 @@ const TideDashboardV2 = () => { return ( <> + + {t('Filter By')} + + + + + + + + { +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?.includes('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 hasRemoveIcon = !required; return ( { fontWeight: 'normal', cursor: 'default', ml: 1, - '&:hover': { + '&:hover': hasRemoveIcon ? { color: vizColors.blue80, fontWeight: 'medium', - }, + } : {}, '.remove-filter-icon': { fontSize: '14px', padding: '2px', @@ -154,12 +156,14 @@ const Chip = ({ label, onRemove }) => { {label} - + { hasRemoveIcon && + + } ); }; @@ -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 ( Date: Mon, 17 Aug 2026 15:33:30 -0700 Subject: [PATCH 003/123] WEB-4460 remove DataRecencyType from Data Recency dropdown via prop --- .../components/DataRecencyFilterDropdown.js | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js index a1c306ee61..36a7297aff 100644 --- a/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js +++ b/app/pages/clinicworkspace/components/DataRecencyFilterDropdown.js @@ -18,6 +18,8 @@ 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 = ({ diff --git a/app/components/navpatientheader/EditPatientDialogController.js b/app/components/navpatientheader/EditPatientDialogController.js new file mode 100644 index 0000000000..3383c5cd2a --- /dev/null +++ b/app/components/navpatientheader/EditPatientDialogController.js @@ -0,0 +1,84 @@ +import React, { useRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import noop from 'lodash/noop'; +import get from 'lodash/get'; +import isEqual from 'lodash/isEqual'; +import { DEFAULT_GLYCEMIC_RANGES } from '../../core/glycemicRangesUtils'; +import { useToasts } from '../../providers/ToastProvider'; +import * as actions from '../../redux/actions'; +import EditPatientDialog from '../clinic/EditPatientDialog'; + +const EditPatientDialogController = ({ + api, + clinicPatient, + isOpen, + onClose = noop, +}) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { set: setToast } = useToasts(); + + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + const currentPatientInViewId = useSelector(state => state.blip.currentPatientInViewId); + const updatingClinicPatient = useSelector((state) => state.blip.working.updatingClinicPatient); + const { notification } = updatingClinicPatient; + + const hasChartData = useSelector(state => (state.blip.data?.metaData?.size || 0) > 0); + + // Captured at submit time: whether this edit needs the chart data reprocessed. + const shouldClearDataRef = useRef(false); + + const onEditSuccess = () => { + // updatingClinicPatient is global working state, so this fires for any clinic-patient update while + // the header is mounted. Only react to updates this dialog drove; a foreign update (e.g. adding a + // data source) would otherwise clear the data worker cache and strand the data view on the loader. + setToast({ + message: t('You have successfully updated a patient.'), + variant: 'success', + }); + + if (!isOpen) return; + + onClose(); + + if (shouldClearDataRef.current) { + dispatch(actions.worker.dataWorkerRemoveDataRequest(null, currentPatientInViewId)); + shouldClearDataRef.current = false; + } + }; + + const onEditFailure = () => { + setToast({ + message: get(notification, 'message'), + variant: 'danger', + }); + }; + + const handleEditPatientConfirm = (formContext) => { + // Clear the data worker (forcing a reprocess) only when Target Range (glycemicRanges, the only + // data-affecting field here) changed AND there is chart data to reprocess. Compare against the + // patient's saved range, not the form's initialValues (frozen at mount, blind to prior saves); + // fall back to the default range so a patient without one — for whom the form injects the default — + // doesn't read as a change. + const savedRange = clinicPatient?.glycemicRanges || DEFAULT_GLYCEMIC_RANGES; + const targetRangeChanged = !isEqual(formContext?.values?.glycemicRanges, savedRange); + shouldClearDataRef.current = targetRangeChanged && hasChartData; + }; + + if (!currentPatientInViewId || !selectedClinicId) return null; + + return ( + + ); +}; + +export default EditPatientDialogController; diff --git a/app/components/navpatientheader/index.js b/app/components/navpatientheader/index.js index 58b6ff0b11..1a3496e610 100644 --- a/app/components/navpatientheader/index.js +++ b/app/components/navpatientheader/index.js @@ -9,7 +9,7 @@ import DemographicInfo from './DemographicInfo'; import PatientMenuOptions from './MenuOptions/Patient'; import ClinicianMenuOptions from './MenuOptions/Clinician'; import UploadLaunchOverlay from '../../components/uploadlaunchoverlay'; -import EditPatientDialog from './EditPatientDialog'; +import EditPatientDialogController from './EditPatientDialogController'; import { isClinicianAccount } from '../../core/personutils'; import { breakpoints } from '../../themes/baseTheme'; @@ -98,7 +98,7 @@ const NavPatientHeader = ({ api, trackMetric, patient, clinicPatient, user, perm setIsUploadOverlayOpen(false)} /> } - { const clinicPatient = patients.find(patient => patient.id === editPatientDialog.patientId); - const handleCloseModal = () => { - dispatch(closeModals()); - }; + const handleCloseModal = () => dispatch(closeModals()); const handleEditSuccess = () => { + dispatch(closeModals()); dispatch(RTKQueryApi.util.invalidateTags([TIDE_DASHBOARD_PATIENTS])); }; From c73e44aab496d9334e56c85d973eea1d87b813b4 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 17:53:32 -0700 Subject: [PATCH 090/123] WEB-4460 add new tests for abstract modal --- .../clinic/EditPatientDialog.test.js | 138 ++++++++++++++++++ .../EditPatientDialogController.test.js | 64 -------- app/components/clinic/EditPatientDialog.js | 2 +- 3 files changed, 139 insertions(+), 65 deletions(-) create mode 100644 __tests__/unit/components/clinic/EditPatientDialog.test.js diff --git a/__tests__/unit/components/clinic/EditPatientDialog.test.js b/__tests__/unit/components/clinic/EditPatientDialog.test.js new file mode 100644 index 0000000000..248e7a06df --- /dev/null +++ b/__tests__/unit/components/clinic/EditPatientDialog.test.js @@ -0,0 +1,138 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import configureStore from 'redux-mock-store'; +import { thunk } from 'redux-thunk'; +import { ThemeProvider } from 'theme-ui'; +import theme from '@app/themes/baseTheme'; +import EditPatientDialog from '@app/components/clinic/EditPatientDialog'; +import { usePrevious } from '@app/core/hooks'; + +const mockStore = configureStore([thunk]); + +jest.mock('@app/core/hooks', () => ({ + ...jest.requireActual('@app/core/hooks'), + useIsFirstRender: jest.fn(() => false), + usePrevious: jest.fn(), +})); + +const clinicPatient = { + id: 'patient123', + fullName: 'John Doe', + birthDate: '2000-01-01', + permissions: { custodian: {} }, +}; + +const IDLE_UPDATE = { inProgress: false, completed: null, notification: null }; +const COMPLETED_UPDATE = { inProgress: false, completed: true, notification: null }; +const FAILED_UPDATE = { inProgress: false, completed: false, notification: { message: 'Something went wrong' } }; + +const makeState = ({ smartCorrelationId, updatingClinicPatient = IDLE_UPDATE } = {}) => ({ + blip: { + selectedClinicId: 'clinic123', + smartCorrelationId, + clinics: { clinic123: { id: 'clinic123', mrnSettings: { required: false } } }, + working: { + fetchingClinicMRNsForPatientFormValidation: { inProgress: false, completed: false, notification: null }, + updatingClinicPatient, + }, + clinicMRNsForPatientFormValidation: [], + }, +}); + +const api = { clinics: { getPatientFromClinic: jest.fn(), updateClinicPatient: jest.fn() } }; +const onClose = jest.fn(); +const onEditConfirm = jest.fn(); +const onEditSuccess = jest.fn(); +const onEditFailure = jest.fn(); + +const ui = (store) => ( + + + + + + + +); + +describe('EditPatientDialog', () => { + beforeEach(() => { + jest.clearAllMocks(); + usePrevious.mockReturnValue(true); + }); + + it('renders the patient form populated from clinicPatient, locking identity fields only in smart-on-fhir mode', () => { + const { rerender } = render(ui(mockStore(makeState()))); + + // The form is prefilled from clinicPatient ('2000-01-01' renders in the display format, 01/01/2000) + expect(screen.getByRole('textbox', { name: /Full Name/i })).toHaveValue('John Doe'); + expect(screen.getByRole('textbox', { name: /Birthdate/i })).toHaveValue('01/01/2000'); + + // Outside smart-on-fhir mode every field is editable + expect(screen.getByRole('textbox', { name: /Full Name/i })).toBeEnabled(); + expect(screen.getByRole('textbox', { name: /Birthdate/i })).toBeEnabled(); + expect(screen.getByRole('textbox', { name: /MRN/i })).toBeEnabled(); + expect(screen.getByRole('textbox', { name: /Email/i })).toBeEnabled(); + + // In smart-on-fhir mode the EHR-sourced identity fields lock + rerender(ui(mockStore(makeState({ smartCorrelationId: 'some-correlation-id' })))); + expect(screen.getByRole('textbox', { name: /Full Name/i })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /Birthdate/i })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /MRN/i })).toBeDisabled(); + expect(screen.getByRole('textbox', { name: /Email/i })).toBeDisabled(); + + // Clinical fields stay editable in smart-on-fhir mode + expect(screen.getByLabelText(/Diabetes Type/i)).toBeEnabled(); + expect(screen.getByLabelText('Target Range')).toBeEnabled(); + }); + + it('notifies the parent through onEditConfirm and submits the form when Save Changes is clicked', async () => { + render(ui(mockStore(makeState()))); + + await userEvent.click(screen.getByRole('button', { name: 'Save Changes' })); + + // The parent is handed the live form context before the submit fires + expect(onEditConfirm).toHaveBeenCalledWith( + expect.objectContaining({ values: expect.objectContaining({ fullName: 'John Doe' }) }) + ); + + // The form submit runs through to the patient update endpoint + await waitFor(() => { + expect(api.clinics.updateClinicPatient).toHaveBeenCalledWith( + 'clinic123', // clinicId + 'patient123', // patientId + expect.objectContaining({ fullName: 'John Doe' }), // updated patient + expect.any(Function), // node-style callback + ); + }); + }); + + it('reports the update result: onEditSuccess when it completes and onEditFailure when it fails', () => { + const { rerender } = render(ui(mockStore(makeState({ updatingClinicPatient: IDLE_UPDATE })))); + + // Nothing is reported while no update has resolved + expect(onEditSuccess).not.toHaveBeenCalled(); + expect(onEditFailure).not.toHaveBeenCalled(); + + // The in-flight update completes successfully + rerender(ui(mockStore(makeState({ updatingClinicPatient: COMPLETED_UPDATE })))); + expect(onEditSuccess).toHaveBeenCalledTimes(1); + expect(onEditFailure).not.toHaveBeenCalled(); + + // A later update fails + rerender(ui(mockStore(makeState({ updatingClinicPatient: FAILED_UPDATE })))); + expect(onEditFailure).toHaveBeenCalledTimes(1); + expect(onEditSuccess).toHaveBeenCalledTimes(1); + }); +}); diff --git a/__tests__/unit/components/navpatientheader/EditPatientDialogController.test.js b/__tests__/unit/components/navpatientheader/EditPatientDialogController.test.js index 33427a0fb6..c20888b652 100644 --- a/__tests__/unit/components/navpatientheader/EditPatientDialogController.test.js +++ b/__tests__/unit/components/navpatientheader/EditPatientDialogController.test.js @@ -45,71 +45,7 @@ const initialState = { }, }; -const renderEditPatientDialogController = (storeState = initialState, clinicPatient) => { - const reducer = (state = storeState, action) => state; - const store = createStore(reducer, applyMiddleware(thunk)); - - return render( - - - - - - - - - - ); -}; - describe('EditPatientDialogController', () => { - it('locks identity fields and leaves the save button enabled when smartCorrelationId is present', () => { - const smartOnFhirState = { - blip: { - ...initialState.blip, - smartCorrelationId: 'some-correlation-id', - clinics: { - clinic123: { - ...initialState.blip.clinics.clinic123, - patients: { - patient123: { id: 'patient123', fullName: 'John Doe', birthDate: '2000-01-01' }, - }, - }, - }, - }, - }; - - renderEditPatientDialogController(smartOnFhirState, baseClinicPatient); - - expect(screen.getByRole('textbox', { name: /Full Name/i })).toBeDisabled(); - expect(screen.getByRole('textbox', { name: /Birthdate/i })).toBeDisabled(); - expect(screen.getByRole('textbox', { name: /MRN/i })).toBeDisabled(); - expect(screen.getByRole('textbox', { name: /Email/i })).toBeDisabled(); - // Clinical fields stay editable in smart-on-fhir mode; only EHR-sourced identity fields lock. - expect(screen.getByLabelText(/Diabetes Type/i)).not.toBeDisabled(); - expect(screen.getByLabelText('Target Range')).not.toBeDisabled(); - - const saveButton = screen.getByRole('button', { name: 'Save Changes' }); - expect(saveButton).toBeInTheDocument(); - expect(saveButton).toBeEnabled(); - }); - - it('sets read-only fields enabled when smartCorrelationId is absent', () => { - renderEditPatientDialogController(initialState, baseClinicPatient); - - expect(screen.getByRole('textbox', { name: /Full Name/i })).not.toBeDisabled(); - expect(screen.getByRole('textbox', { name: /Birthdate/i })).not.toBeDisabled(); - expect(screen.getByRole('textbox', { name: /MRN/i })).not.toBeDisabled(); - expect(screen.getByRole('textbox', { name: /Email/i })).not.toBeDisabled(); - expect(screen.getByLabelText(/Diabetes Type/i)).not.toBeDisabled(); - expect(screen.getByLabelText('Target Range')).not.toBeDisabled(); - }); - // The dialog is permanently mounted and subscribes to the global updatingClinicPatient, so it must // only clear the data worker for updates it initiated (isOpen), not foreign ones (e.g. adding a data // source) — clearing on a foreign update would strand the patient-data view on the loader. diff --git a/app/components/clinic/EditPatientDialog.js b/app/components/clinic/EditPatientDialog.js index 5636797847..ba99bcd2eb 100644 --- a/app/components/clinic/EditPatientDialog.js +++ b/app/components/clinic/EditPatientDialog.js @@ -91,7 +91,7 @@ const EditPatientDialog = ({ onClose={onClose} > { - trackMetric('Clinic - Edit patient close', { clinicId: selectedClinicId }); + trackMetric('Clinic - Edit patient close', { clinicId: selectedClinicId, pageName }); onClose(); }}> {t('Edit Patient Details')} From ee6d1d88963e9336925e20b4d5196722774361a3 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 18:00:43 -0700 Subject: [PATCH 091/123] WEB-4460 use convention for handler names --- .../navpatientheader/EditPatientDialogController.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/components/navpatientheader/EditPatientDialogController.js b/app/components/navpatientheader/EditPatientDialogController.js index 3383c5cd2a..922f34b980 100644 --- a/app/components/navpatientheader/EditPatientDialogController.js +++ b/app/components/navpatientheader/EditPatientDialogController.js @@ -29,7 +29,7 @@ const EditPatientDialogController = ({ // Captured at submit time: whether this edit needs the chart data reprocessed. const shouldClearDataRef = useRef(false); - const onEditSuccess = () => { + const handleEditSuccess = () => { // updatingClinicPatient is global working state, so this fires for any clinic-patient update while // the header is mounted. Only react to updates this dialog drove; a foreign update (e.g. adding a // data source) would otherwise clear the data worker cache and strand the data view on the loader. @@ -48,14 +48,14 @@ const EditPatientDialogController = ({ } }; - const onEditFailure = () => { + const handleEditFailure = () => { setToast({ message: get(notification, 'message'), variant: 'danger', }); }; - const handleEditPatientConfirm = (formContext) => { + const handleEditConfirm = (formContext) => { // Clear the data worker (forcing a reprocess) only when Target Range (glycemicRanges, the only // data-affecting field here) changed AND there is chart data to reprocess. Compare against the // patient's saved range, not the form's initialValues (frozen at mount, blind to prior saves); @@ -74,9 +74,9 @@ const EditPatientDialogController = ({ clinicPatient={clinicPatient} isOpen={isOpen} onClose={onClose} - onEditConfirm={handleEditPatientConfirm} - onEditSuccess={onEditSuccess} - onEditFailure={onEditFailure} + onEditConfirm={handleEditConfirm} + onEditSuccess={handleEditSuccess} + onEditFailure={handleEditFailure} /> ); }; From 155e99150b077ba9ab88a3fe0bb268f3f57cf8c0 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 18:10:15 -0700 Subject: [PATCH 092/123] WEB-4460 fix imports --- .../DataIssues/EditPatientDialogController.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/EditPatientDialogController.js diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/EditPatientDialogController.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/EditPatientDialogController.js new file mode 100644 index 0000000000..75d2b5c648 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/EditPatientDialogController.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { RTKQueryApi } from '../../../../redux/api/baseApi'; +import EditPatientDialog from '../../../../components/clinic/EditPatientDialog'; +import { tagTypes } from '../tideDashboardApi'; + +const { TIDE_DASHBOARD_PATIENTS } = tagTypes; + +const EditPatientDialogController = ({ api, isOpen, patient, onClose }) => { + const dispatch = useDispatch(); + + const handleEditSuccess = () => { + dispatch(RTKQueryApi.util.invalidateTags([TIDE_DASHBOARD_PATIENTS])); + }; + + return ( + + ); +}; + +export default EditPatientDialogController; From 8ada273c2fd52b20e67d42fc76f6e8c2f3cd2261 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 3 Sep 2026 17:50:20 -0700 Subject: [PATCH 093/123] WEB-4460 address automated review feedback --- app/components/datasources/DataConnectionsModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/datasources/DataConnectionsModal.js b/app/components/datasources/DataConnectionsModal.js index 2fd5f83168..e3b3227e69 100644 --- a/app/components/datasources/DataConnectionsModal.js +++ b/app/components/datasources/DataConnectionsModal.js @@ -28,6 +28,7 @@ import i18next from '../../core/language'; import { URL_TIDEPOOL_EXTERNAL_DATA_CONNECTIONS, URL_UPLOADER_DOWNLOAD_PAGE } from '../../core/constants'; import PatientEmailModal from './PatientEmailModal'; import { DesktopOnly } from '../mediaqueries'; +import { trackMetric } from '../../core/metricUtils'; const t = i18next.t.bind(i18next); @@ -38,7 +39,6 @@ export const DataConnectionsModal = (props) => { onBack, patient, shownProviders, - trackMetric, } = props; const history = useHistory(); From 0100ed66239012eb2156c241a3a524ed39ad45b3 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 18 Aug 2026 12:58:09 -0700 Subject: [PATCH 094/123] WEB-4460 pull in PatientDrawer --- .../PatientDrawerController.js | 30 +++++++++++++++++++ .../TideDashboardV2/TideDashboardV2.js | 18 +++++++++++ 2 files changed, 48 insertions(+) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js b/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js new file mode 100644 index 0000000000..3129a958aa --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { useLocation, useHistory } from 'react-router-dom'; +import PatientDrawer from '../../../components/PatientDrawer'; + +const PatientDrawerController = ({ api }) => { + const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); + const { search, pathname } = useLocation(); + const history = useHistory(); + + const drawerPatientId = new URLSearchParams(search)?.get('drawerPatientId') || null; + + const handleClose = () => { + const params = new URLSearchParams(search); + params.delete('drawerPatientId'); + params.delete('drawerTab'); + history.replace({ pathname, search: params.toString() }); + }; + + return ( + + ); +}; + +export default PatientDrawerController; diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index ac4d4cac99..97e54a4d3d 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; +import { useLocation, useHistory } from 'react-router-dom'; import Table from '../../../components/elements/Table'; import { Flex, Text, Box } from 'theme-ui'; @@ -21,8 +22,10 @@ import EmptyContentNode from './EmptyContentNode'; import { Redirect, useLocation } from 'react-router-dom'; import useAuthorizationGate from './useAuthorizationGate'; +import PatientDrawerController from './PatientDrawerController'; import EditPatientDialogController from './modals/EditPatientDialogController'; import DataConnectionsModalController from './modals/DataConnectionsModalController'; +import { OVERVIEW_TAB_INDEX } from '../../../components/PatientDrawer/MenuBar'; const Gap = () => ; @@ -31,6 +34,8 @@ const tableContainerProps = { sx: { containerType: 'inline-size' } }; const TideDashboardV2 = ({ api }) => { const { search } = useLocation(); const { t } = useTranslation(); + const { search, pathname } = useLocation(); + const history = useHistory(); usePruneInvalidFilters(); @@ -52,6 +57,17 @@ const TideDashboardV2 = ({ api }) => { if (!isAuthorized || !data) return null; + const handleClickRow = (patient) => { + if (!patient.id) return; + + const params = new URLSearchParams(search); + params.set('drawerPatientId', patient.id); + params.set('drawerTab', OVERVIEW_TAB_INDEX); + history.replace({ pathname, search: params.toString() }); + }; + + if (!data) return null; + const patients = data?.data || []; const total = data?.meta?.count || 0; @@ -80,10 +96,12 @@ const TideDashboardV2 = ({ api }) => { data={patients} emptyContentNode={emptyContentNode} containerProps={tableContainerProps} + onClickRow={handleClickRow} /> + From fc74a59b100ceeb1f244ef4e86bf5fb3c94b5cd4 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 18 Aug 2026 12:59:29 -0700 Subject: [PATCH 095/123] WEB-4460 move PatientDrawer directory to components --- .../PatientDrawer/CGMDeltaSummary/index.js | 0 .../dashboard => components}/PatientDrawer/CGMStatistics/index.js | 0 .../PatientDrawer/MenuBar/CGMClipboardButton.js | 0 .../dashboard => components}/PatientDrawer/MenuBar/MenuBar.js | 0 .../dashboard => components}/PatientDrawer/MenuBar/index.js | 0 app/{pages/dashboard => components}/PatientDrawer/Overview.js | 0 .../dashboard => components}/PatientDrawer/PatientDrawer.js | 0 app/{pages/dashboard => components}/PatientDrawer/StackedDaily.js | 0 .../dashboard => components}/PatientDrawer/getReportDaysText.js | 0 app/{pages/dashboard => components}/PatientDrawer/index.js | 0 .../dashboard => components}/PatientDrawer/useAgpCGM/getOpts.js | 0 .../PatientDrawer/useAgpCGM/getQueries.js | 0 .../dashboard => components}/PatientDrawer/useAgpCGM/index.js | 0 .../dashboard => components}/PatientDrawer/useAgpCGM/useAgpCGM.js | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename app/{pages/dashboard => components}/PatientDrawer/CGMDeltaSummary/index.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/CGMStatistics/index.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/MenuBar/CGMClipboardButton.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/MenuBar/MenuBar.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/MenuBar/index.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/Overview.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/PatientDrawer.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/StackedDaily.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/getReportDaysText.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/index.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/useAgpCGM/getOpts.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/useAgpCGM/getQueries.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/useAgpCGM/index.js (100%) rename app/{pages/dashboard => components}/PatientDrawer/useAgpCGM/useAgpCGM.js (100%) diff --git a/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.js b/app/components/PatientDrawer/CGMDeltaSummary/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.js rename to app/components/PatientDrawer/CGMDeltaSummary/index.js diff --git a/app/pages/dashboard/PatientDrawer/CGMStatistics/index.js b/app/components/PatientDrawer/CGMStatistics/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/CGMStatistics/index.js rename to app/components/PatientDrawer/CGMStatistics/index.js diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js b/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js rename to app/components/PatientDrawer/MenuBar/CGMClipboardButton.js diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js rename to app/components/PatientDrawer/MenuBar/MenuBar.js diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/index.js b/app/components/PatientDrawer/MenuBar/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/MenuBar/index.js rename to app/components/PatientDrawer/MenuBar/index.js diff --git a/app/pages/dashboard/PatientDrawer/Overview.js b/app/components/PatientDrawer/Overview.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/Overview.js rename to app/components/PatientDrawer/Overview.js diff --git a/app/pages/dashboard/PatientDrawer/PatientDrawer.js b/app/components/PatientDrawer/PatientDrawer.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/PatientDrawer.js rename to app/components/PatientDrawer/PatientDrawer.js diff --git a/app/pages/dashboard/PatientDrawer/StackedDaily.js b/app/components/PatientDrawer/StackedDaily.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/StackedDaily.js rename to app/components/PatientDrawer/StackedDaily.js diff --git a/app/pages/dashboard/PatientDrawer/getReportDaysText.js b/app/components/PatientDrawer/getReportDaysText.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/getReportDaysText.js rename to app/components/PatientDrawer/getReportDaysText.js diff --git a/app/pages/dashboard/PatientDrawer/index.js b/app/components/PatientDrawer/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/index.js rename to app/components/PatientDrawer/index.js diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js b/app/components/PatientDrawer/useAgpCGM/getOpts.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js rename to app/components/PatientDrawer/useAgpCGM/getOpts.js diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js b/app/components/PatientDrawer/useAgpCGM/getQueries.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js rename to app/components/PatientDrawer/useAgpCGM/getQueries.js diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/index.js b/app/components/PatientDrawer/useAgpCGM/index.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/index.js rename to app/components/PatientDrawer/useAgpCGM/index.js diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js similarity index 100% rename from app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js rename to app/components/PatientDrawer/useAgpCGM/useAgpCGM.js From bb8606171d46afb90aaee9b9fed725b4fbd7eb51 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 18 Aug 2026 13:14:49 -0700 Subject: [PATCH 096/123] WEB-4460 update drawer import directories --- .../PatientDrawer/CGMDeltaSummary/index.js | 2 +- .../PatientDrawer/CGMStatistics/index.js | 2 +- .../MenuBar/CGMClipboardButton.js | 4 ++-- .../PatientDrawer/MenuBar/MenuBar.js | 12 ++++++------ app/components/PatientDrawer/PatientDrawer.js | 18 +++++++----------- app/components/PatientDrawer/StackedDaily.js | 8 ++++---- .../PatientDrawer/getReportDaysText.js | 2 +- .../PatientDrawer/useAgpCGM/getOpts.js | 4 ++-- .../PatientDrawer/useAgpCGM/getQueries.js | 4 ++-- .../PatientDrawer/useAgpCGM/useAgpCGM.js | 4 ++-- app/pages/dashboard/TideDashboard.js | 4 ++-- 11 files changed, 30 insertions(+), 34 deletions(-) diff --git a/app/components/PatientDrawer/CGMDeltaSummary/index.js b/app/components/PatientDrawer/CGMDeltaSummary/index.js index 238b1ec451..ed38fbb8e1 100644 --- a/app/components/PatientDrawer/CGMDeltaSummary/index.js +++ b/app/components/PatientDrawer/CGMDeltaSummary/index.js @@ -6,7 +6,7 @@ import { utils as vizUtils, colors as vizColors } from '@tidepool/viz'; import styled from '@emotion/styled'; const { bankersRound } = vizUtils.stat; const { getTimezoneFromTimePrefs } = vizUtils.datetime; -import { MS_IN_HOUR } from '../../../../core/constants'; +import { MS_IN_HOUR } from '../../../core/constants'; import getReportDaysText from '../getReportDaysText'; diff --git a/app/components/PatientDrawer/CGMStatistics/index.js b/app/components/PatientDrawer/CGMStatistics/index.js index 5645dbab2b..45e9826a72 100644 --- a/app/components/PatientDrawer/CGMStatistics/index.js +++ b/app/components/PatientDrawer/CGMStatistics/index.js @@ -5,7 +5,7 @@ import { Flex, Box, Text } from 'theme-ui'; import { utils as vizUtils } from '@tidepool/viz'; const { formatDatum, bankersRound } = vizUtils.stat; const { getTimezoneFromTimePrefs } = vizUtils.datetime; -import { MGDL_UNITS } from '../../../../core/constants'; +import { MGDL_UNITS } from '../../../core/constants'; import getReportDaysText from '../getReportDaysText'; const TableRow = ({ label, sublabel, value, units, id }) => { diff --git a/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js b/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js index bc17372335..6dd7cf9060 100644 --- a/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js +++ b/app/components/PatientDrawer/MenuBar/CGMClipboardButton.js @@ -1,8 +1,8 @@ import React, { useEffect, useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; -import Button from '../../../../components/elements/Button'; -import { MS_IN_HOUR } from '../../../../core/constants'; +import Button from '../../../components/elements/Button'; +import { MS_IN_HOUR } from '../../../core/constants'; import { Box, Flex } from 'theme-ui'; import { utils as vizUtils } from '@tidepool/viz'; const { agpCGMText } = vizUtils.text; diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index 9f013bc068..b8d858ed0a 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -1,20 +1,20 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import * as actions from '../../../../redux/actions'; +import * as actions from '../../../redux/actions'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { push } from 'connected-react-router'; import { Flex, Box, Text } from 'theme-ui'; import { colors as vizColors } from '@tidepool/viz'; -import Button from '../../../../components/elements/Button'; +import Button from '../../../components/elements/Button'; import moment from 'moment'; import PatientLastReviewed from '../../../clinicworkspace/components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; import CGMClipboardButton from './CGMClipboardButton'; -import api from '../../../../core/api'; +import api from '../../../core/api'; import { map, keys } from 'lodash'; -import copyIcon from '../../../../core/icons/copyIcon.svg'; -import viewIcon from '../../../../core/icons/viewIcon.svg'; -import { trackMetric } from '../../../../core/metricUtils'; +import copyIcon from '../../../core/icons/copyIcon.svg'; +import viewIcon from '../../../core/icons/viewIcon.svg'; +import { trackMetric } from '../../../core/metricUtils'; export const OVERVIEW_TAB_INDEX = 0; export const STACKED_DAILY_TAB_INDEX = 1; diff --git a/app/components/PatientDrawer/PatientDrawer.js b/app/components/PatientDrawer/PatientDrawer.js index 92df8f3673..60a1bea5da 100644 --- a/app/components/PatientDrawer/PatientDrawer.js +++ b/app/components/PatientDrawer/PatientDrawer.js @@ -1,19 +1,18 @@ import React from 'react'; -import Icon from '../../../components/elements/Icon'; +import Icon from '../../components/elements/Icon'; import { useLocation, useHistory } from 'react-router-dom'; import Drawer from '@material-ui/core/Drawer'; import styled from '@emotion/styled'; import { makeStyles } from '@material-ui/core/styles'; import CloseRoundedIcon from '@material-ui/icons/CloseRounded'; import { Box } from 'theme-ui'; -import { useFlags } from 'launchdarkly-react-client-sdk'; import Overview from './Overview'; import StackedDaily from './StackedDaily'; import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from './MenuBar'; import useAgpCGM from './useAgpCGM'; -import { shadows } from '../../../themes/baseTheme'; -import { useScrollToTop } from '../../../core/hooks'; +import { shadows } from '../../themes/baseTheme'; +import { useScrollToTop } from '../../core/hooks'; const StyledCloseButton = styled(Icon)` position: absolute; @@ -53,7 +52,7 @@ const getAgpPeriodInDays = (period) => { } }; -const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { +const DrawerContent = ({ patientId, onClose, api, period }) => { // Only rendered when patient is selected and isOpen is true // this will also allow the hook to dismount and for the cleanup to be called const location = useLocation(); @@ -85,7 +84,7 @@ const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { return ( <> - + @@ -108,13 +107,10 @@ const DrawerContent = ({ patientId, onClose, api, trackMetric, period }) => { ) } -const PatientDrawer = ({ patientId, onClose, api, trackMetric, period }) => { +const PatientDrawer = ({ patientId, onClose, api, period }) => { const classes = useStyles(); - const { showTideDashboardPatientDrawer } = useFlags(); const isOpen = !!patientId && isValidAgpPeriod(period); - if (!showTideDashboardPatientDrawer) return null; - return ( { flexDirection: 'column', }} > - {isOpen && } + {isOpen && } ); diff --git a/app/components/PatientDrawer/StackedDaily.js b/app/components/PatientDrawer/StackedDaily.js index 246c50175c..d09fab05ea 100644 --- a/app/components/PatientDrawer/StackedDaily.js +++ b/app/components/PatientDrawer/StackedDaily.js @@ -15,13 +15,13 @@ const { getLocalizedCeiling } = vizUtils.datetime; import tidelineBlip from 'tideline/plugins/blip'; const chartDailyFactory = tidelineBlip.oneday; -import { MS_IN_DAY } from '../../../core/constants'; +import { MS_IN_DAY } from '../../core/constants'; import { NoPatientData } from './Overview'; import { STATUS } from './useAgpCGM'; -import { Body1, Body2 } from '../../../components/elements/FontStyles'; -import Button from '../../../components/elements/Button'; +import { Body1, Body2 } from '../../components/elements/FontStyles'; +import Button from '../../components/elements/Button'; import { STACKED_DAILY_TAB_INDEX } from './MenuBar'; -import BgLegend from '../../../components/chart/BgLegend'; +import BgLegend from '../../components/chart/BgLegend'; const CHART_HEIGHT = 200; diff --git a/app/components/PatientDrawer/getReportDaysText.js b/app/components/PatientDrawer/getReportDaysText.js index 7e88335997..36f62dc66a 100644 --- a/app/components/PatientDrawer/getReportDaysText.js +++ b/app/components/PatientDrawer/getReportDaysText.js @@ -1,5 +1,5 @@ import moment from 'moment'; -import { MS_IN_MIN } from '../../../core/constants'; +import { MS_IN_MIN } from '../../core/constants'; import isNumber from 'lodash/isNumber'; import { utils as vizUtils } from '@tidepool/viz'; const { getOffset, formatDateRange } = vizUtils.datetime; diff --git a/app/components/PatientDrawer/useAgpCGM/getOpts.js b/app/components/PatientDrawer/useAgpCGM/getOpts.js index f97ece5587..d3597c9f2f 100644 --- a/app/components/PatientDrawer/useAgpCGM/getOpts.js +++ b/app/components/PatientDrawer/useAgpCGM/getOpts.js @@ -2,8 +2,8 @@ import moment from 'moment-timezone'; import _ from 'lodash'; import get from 'lodash/get'; import { utils as vizUtils } from '@tidepool/viz'; -import utils from '../../../../core/utils'; -import { getMostRecentDatumTimeByChartType } from '../../../../core/dataViewUtils'; +import utils from '../../../core/utils'; +import { getMostRecentDatumTimeByChartType } from '../../../core/dataViewUtils'; const getTimezoneFromTimePrefs = vizUtils.datetime.getTimezoneFromTimePrefs; diff --git a/app/components/PatientDrawer/useAgpCGM/getQueries.js b/app/components/PatientDrawer/useAgpCGM/getQueries.js index 6d6750c203..1d1af429f0 100644 --- a/app/components/PatientDrawer/useAgpCGM/getQueries.js +++ b/app/components/PatientDrawer/useAgpCGM/getQueries.js @@ -2,8 +2,8 @@ import _ from 'lodash'; import { utils as vizUtils } from '@tidepool/viz'; const { commonStats } = vizUtils.stat; -import utils from '../../../../core/utils'; -import { DEFAULT_GLYCEMIC_RANGES } from '../../../../core/glycemicRangesUtils'; +import utils from '../../../core/utils'; +import { DEFAULT_GLYCEMIC_RANGES } from '../../../core/glycemicRangesUtils'; const getQueries = ( data, diff --git a/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js index 5104fbdc3f..e72f0a6dfa 100644 --- a/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js +++ b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js @@ -1,12 +1,12 @@ import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import * as actions from '../../../../redux/actions'; +import * as actions from '../../../redux/actions'; import moment from 'moment'; import getOpts from './getOpts'; import getQueries from './getQueries'; import { cloneDeep } from 'lodash'; -import { useGenerateAGPImages } from '../../../../core/agpUtils'; +import { useGenerateAGPImages } from '../../../core/agpUtils'; export const STATUS = { // States in order of happy path AGP generation sequence diff --git a/app/pages/dashboard/TideDashboard.js b/app/pages/dashboard/TideDashboard.js index bd0f8e5173..c5a53b158f 100644 --- a/app/pages/dashboard/TideDashboard.js +++ b/app/pages/dashboard/TideDashboard.js @@ -58,7 +58,7 @@ import PopoverMenu from '../../components/elements/PopoverMenu'; import RadioGroup from '../../components/elements/RadioGroup'; import DeltaBar from '../../components/elements/DeltaBar'; import Pill from '../../components/elements/Pill'; -import PatientDrawer, { isValidAgpPeriod } from './PatientDrawer'; +import PatientDrawer, { isValidAgpPeriod } from '../../components/PatientDrawer'; import utils from '../../core/utils'; import { @@ -85,7 +85,7 @@ import DataInIcon from '../../core/icons/DataInIcon.svg'; import { colors, fontWeights, radii } from '../../themes/baseTheme'; import PatientLastReviewed from '../../components/clinic/PatientLastReviewed'; import { DEFAULT_GLYCEMIC_RANGES } from '../../core/glycemicRangesUtils'; -import { OVERVIEW_TAB_INDEX } from './PatientDrawer/MenuBar/MenuBar'; +import { OVERVIEW_TAB_INDEX } from '../../components/PatientDrawer/MenuBar/MenuBar'; const { Loader } = vizComponents; const { formatBgValue } = vizUtils.bg; From ec006b39a3e3fa55cc890e24e0f28eb127689fe6 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 18 Aug 2026 14:36:16 -0700 Subject: [PATCH 097/123] WEB-4460 fix MenuBar pulling in correct PatientLastReviewed --- app/components/PatientDrawer/MenuBar/MenuBar.js | 11 +++++------ app/components/clinic/PatientLastReviewed.js | 3 --- .../ReviewPatientToggle/reviewPatientApi.js | 6 ++++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index b8d858ed0a..33b0ca8257 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -10,7 +10,7 @@ import Button from '../../../components/elements/Button'; import moment from 'moment'; import PatientLastReviewed from '../../../clinicworkspace/components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; import CGMClipboardButton from './CGMClipboardButton'; -import api from '../../../core/api'; +import { useGetPatientDrawerPatientQuery } from '../patientDrawerApi'; import { map, keys } from 'lodash'; import copyIcon from '../../../core/icons/copyIcon.svg'; import viewIcon from '../../../core/icons/viewIcon.svg'; @@ -37,13 +37,12 @@ const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => { const { t } = useTranslation(); const selectedClinicId = useSelector(state => state.blip.selectedClinicId); - const patient = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]?.patients?.[patientId]); const pdf = useSelector(state => state.blip.pdf); // IMPORTANT: Data taken from Redux PDF slice - useEffect(() => { - // DOB field in Patient object may not be populated in TIDE Dashboard, so we need to refetch - dispatch(actions.async.fetchPatientFromClinic(api, selectedClinicId, patientId)); - }, []); + const { data: patient } = useGetPatientDrawerPatientQuery( + { clinicId: selectedClinicId, patientId }, + { skip: !selectedClinicId || !patientId } + ); const handleViewData = () => { dispatch(push(`/patients/${patientId}/data/trends?dashboard=tide&drawerTab=${selectedTab}`)); diff --git a/app/components/clinic/PatientLastReviewed.js b/app/components/clinic/PatientLastReviewed.js index 4e30bc87fe..e4feedc48f 100644 --- a/app/components/clinic/PatientLastReviewed.js +++ b/app/components/clinic/PatientLastReviewed.js @@ -17,8 +17,6 @@ export const PatientLastReviewed = ({ api, patientId }) => { const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); const patient = clinic?.patients?.[patientId]; - const recentlyReviewedThresholdDate = moment().startOf('day').toISOString(); - const { settingClinicPatientLastReviewed, revertingClinicPatientLastReviewed, @@ -79,7 +77,6 @@ export const PatientLastReviewed = ({ api, patientId }) => { PatientLastReviewed.propTypes = { api: PropTypes.object.isRequired, patientId: PropTypes.string.isRequired, - onReview: PropTypes.func, }; export default PatientLastReviewed; diff --git a/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js b/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js index cf0cac7b8d..d8203ef478 100644 --- a/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js +++ b/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js @@ -1,7 +1,9 @@ import { RTKQueryApi } from '../../../../redux/api/baseApi'; import { tagTypes as tideDashboardTagTypes } from '../../TideDashboardV2/tideDashboardApi'; +import { tagTypes as patientDrawerTagTypes } from '../../../../components/PatientDrawer/patientDrawerApi'; const { TIDE_DASHBOARD_PATIENTS } = tideDashboardTagTypes; +const { PATIENT_DRAWER_PATIENT } = patientDrawerTagTypes; const reviewPatientApi = RTKQueryApi.injectEndpoints({ endpoints: (builder) => ({ @@ -10,14 +12,14 @@ const reviewPatientApi = RTKQueryApi.injectEndpoints({ url: `/clinics/${clinicId}/patients/${patientId}/reviews`, method: 'PUT', }), - invalidatesTags: [TIDE_DASHBOARD_PATIENTS], + invalidatesTags: [TIDE_DASHBOARD_PATIENTS, PATIENT_DRAWER_PATIENT], }), undoPatientReviewed: builder.mutation({ query: ({ clinicId, patientId }) => ({ url: `/clinics/${clinicId}/patients/${patientId}/reviews`, method: 'DELETE', }), - invalidatesTags: [TIDE_DASHBOARD_PATIENTS], + invalidatesTags: [TIDE_DASHBOARD_PATIENTS, PATIENT_DRAWER_PATIENT], }), }), }); From bfb5d9fa65b75929a4cfa0eaead5b62181c6d609 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 19 Aug 2026 16:20:18 -0700 Subject: [PATCH 098/123] WEB-4460 add MenuBar PatientLastReviewed --- .../MenuBar/PatientLastReviewed.js | 65 +++++++++++++++++++ app/components/clinic/PatientLastReviewed.js | 2 + 2 files changed, 67 insertions(+) create mode 100644 app/components/PatientDrawer/MenuBar/PatientLastReviewed.js diff --git a/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js b/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js new file mode 100644 index 0000000000..8bf496fb77 --- /dev/null +++ b/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import moment from 'moment-timezone'; +import noop from 'lodash/noop'; + +import ReviewPatientToggle, { + useMarkPatientReviewedMutation, + useUndoPatientReviewedMutation, +} from '../../../pages/clinicworkspace/components/ReviewPatientToggle'; + +import * as ErrorMessages from '../../../redux/constants/errorMessages'; +import { useToasts } from '../../../providers/ToastProvider'; + +const PatientLastReviewed = ({ patient, onReview = noop }) => { + const { set: setToast } = useToasts(); + const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); + const patientId = patient?.id; + + const recentlyReviewedThresholdDate = moment().startOf('isoWeek').toISOString(); + + const [reviews, setReviews] = useState(patient?.reviews || []); + + useEffect(() => { // Sync up if prop updates + setReviews(patient?.reviews || []); + }, [patientId, patient?.reviews]); + + const [markPatientReviewed, { isLoading: isMarking }] = useMarkPatientReviewedMutation(); + const [undoPatientReviewed, { isLoading: isUndoing }] = useUndoPatientReviewedMutation(); + + const handleReview = () => { + markPatientReviewed({ clinicId: selectedClinicId, patientId }) + .unwrap() + .then(updatedReviews => { + setReviews(updatedReviews || []); + onReview(); + }) + .catch(() => setToast({ message: ErrorMessages.ERR_SETTING_CLINIC_PATIENT_LAST_REVIEWED, variant: 'danger' })); + }; + + const handleUndo = () => { + undoPatientReviewed({ clinicId: selectedClinicId, patientId }) + .unwrap() + .then(updatedReviews => setReviews(updatedReviews || [])) + .catch(() => setToast({ message: ErrorMessages.ERR_REVERTING_CLINIC_PATIENT_LAST_REVIEWED, variant: 'danger' })); + }; + + return ( + + ); +}; + +PatientLastReviewed.propTypes = { + patient: PropTypes.object, + onReview: PropTypes.func, +}; + +export default PatientLastReviewed; diff --git a/app/components/clinic/PatientLastReviewed.js b/app/components/clinic/PatientLastReviewed.js index e4feedc48f..5859fbee95 100644 --- a/app/components/clinic/PatientLastReviewed.js +++ b/app/components/clinic/PatientLastReviewed.js @@ -17,6 +17,8 @@ export const PatientLastReviewed = ({ api, patientId }) => { const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]); const patient = clinic?.patients?.[patientId]; + const recentlyReviewedThresholdDate = moment().startOf('day').toISOString(); + const { settingClinicPatientLastReviewed, revertingClinicPatientLastReviewed, From c6251ee38714039db1808769c8f9b5ec7e32e3f5 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Wed, 19 Aug 2026 16:31:19 -0700 Subject: [PATCH 099/123] WEB-4460 wrap lastReviewed in a container that calls stopPropogation() --- app/pages/clinicworkspace/TideDashboardV2/Cells.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/Cells.js index 2cc6eb16fc..fb3cf00d2e 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/Cells.js @@ -296,7 +296,9 @@ export const MoreMenuCell = ({ patient }) => { }; export const PatientLastReviewedCell = ({ patient }) => { - return ; + return event.stopPropagation()}> + + ; }; export default { From ad189e45b8b6a9dadd26f414fa366e019a2338ff Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 11:47:18 -0700 Subject: [PATCH 100/123] WEB-4460 add back in patientDrawerApi --- .../PatientDrawer/patientDrawerApi.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/components/PatientDrawer/patientDrawerApi.js diff --git a/app/components/PatientDrawer/patientDrawerApi.js b/app/components/PatientDrawer/patientDrawerApi.js new file mode 100644 index 0000000000..0863ce6e21 --- /dev/null +++ b/app/components/PatientDrawer/patientDrawerApi.js @@ -0,0 +1,26 @@ +import { RTKQueryApi } from '../../redux/api/baseApi'; + +export const tagTypes = { + PATIENT_DRAWER_PATIENT: 'PATIENT_DRAWER_PATIENT', +}; + +const { PATIENT_DRAWER_PATIENT } = tagTypes; + +RTKQueryApi.enhanceEndpoints({ + addTagTypes: [PATIENT_DRAWER_PATIENT], +}); + +const patientDrawerApi = RTKQueryApi.injectEndpoints({ + endpoints: (builder) => ({ + getPatientDrawerPatient: builder.query({ + query: ({ clinicId, patientId }) => ({ + url: `/clinics/${clinicId}/patients/${patientId}`, + }), + providesTags: [PATIENT_DRAWER_PATIENT], + }), + }), +}); + +export const { + useGetPatientDrawerPatientQuery, +} = patientDrawerApi; From 887f56083e38c1e91ecf29aa697e4edf11027b23 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 15:07:37 -0700 Subject: [PATCH 101/123] WEB-4460 fix breaking tests due to modified directory structure --- .../CGMDeltaSummary/index.test.js | 2 +- .../PatientDrawer/CGMStatistics/index.test.js | 2 +- .../dashboard/PatientDrawer/Overview.test.js | 4 +- .../PatientDrawer/useAgpCGM/useAgpCGM.test.js | 2 +- .../PatientDrawer/MenuBar/MenuBar.test.js | 51 ++++++------------- .../StackedDaily/StackedDaily.test.js | 6 +-- .../PatientDrawer/MenuBar/MenuBar.js | 4 +- .../MenuBar/CGMClipboardButton.test.js | 2 +- 8 files changed, 25 insertions(+), 48 deletions(-) diff --git a/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.test.js b/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.test.js index 1afb18e3e4..cb6bf7a0e6 100644 --- a/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.test.js +++ b/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMDeltaSummary/index.test.js @@ -8,7 +8,7 @@ import React from 'react'; import { render, screen, within } from '@testing-library/react'; -import CGMDeltaSummary from '@app/pages/dashboard/PatientDrawer/CGMDeltaSummary'; +import CGMDeltaSummary from '@app/components/PatientDrawer/CGMDeltaSummary'; const agpCGM = { timePrefs: { diff --git a/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js b/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js index dc15112601..2f3e6e6011 100644 --- a/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js +++ b/__tests__/unit/app/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js @@ -8,7 +8,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import CGMStatistics from '@app/pages/dashboard/PatientDrawer/CGMStatistics'; +import CGMStatistics from '@app/components/PatientDrawer/CGMStatistics'; const agpCGM = { 'data': { diff --git a/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js b/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js index bfeed11c15..fb88fc4d6e 100644 --- a/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js +++ b/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js @@ -10,9 +10,9 @@ import configureStore from 'redux-mock-store'; import { Provider } from 'react-redux'; import { thunk } from 'redux-thunk'; -import Overview from '@app/pages/dashboard/PatientDrawer/Overview'; +import Overview from '@app/components/PatientDrawer/Overview'; -import { STATUS } from '@app/pages/dashboard/PatientDrawer/useAgpCGM'; +import { STATUS } from '@app/components/PatientDrawer/useAgpCGM'; const mockStore = configureStore([thunk]); diff --git a/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js b/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js index e5c1e40d9c..2a14fd7cda 100644 --- a/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js +++ b/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js @@ -16,7 +16,7 @@ import configureStore from 'redux-mock-store'; import { utils as vizUtils } from '@tidepool/viz'; import Plotly from 'plotly.js-basic-dist-min'; -import useAgpCGM from '@app/pages/dashboard/PatientDrawer/useAgpCGM'; +import useAgpCGM from '@app/components/PatientDrawer/useAgpCGM'; import * as actions from '@app/redux/actions'; const mockStore = configureStore([thunk]); diff --git a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js index d81bcc0ae8..9f62196b3c 100644 --- a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js +++ b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js @@ -10,15 +10,12 @@ import { thunk } from 'redux-thunk'; import { I18nextProvider } from 'react-i18next'; import { trackMetric as mockTrackMetric } from '../../../../../../app/core/metricUtils'; -import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from '@app/pages/dashboard/PatientDrawer/MenuBar/MenuBar'; +import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from '@app/components/PatientDrawer/MenuBar/MenuBar'; +import { useGetPatientDrawerPatientQuery } from '@app/components/PatientDrawer/patientDrawerApi'; import i18n from '@app/core/language'; // Mock actions -jest.mock('@app/redux/actions', () => ({ - async: { - fetchPatientFromClinic: jest.fn(() => ({ type: 'MOCK_FETCH_PATIENT' })), - }, -})); +jest.mock('@app/redux/actions', () => ({})); // Mock connected-react-router jest.mock('connected-react-router', () => ({ @@ -28,6 +25,10 @@ jest.mock('connected-react-router', () => ({ // Mock api jest.mock('@app/core/api', () => ({})); +jest.mock('@app/components/PatientDrawer/patientDrawerApi', () => ({ + useGetPatientDrawerPatientQuery: jest.fn(), +})); + // Mock the agpCGMText function from @tidepool/viz while preserving other exports jest.mock('@tidepool/viz', () => { const originalModule = jest.requireActual('@tidepool/viz'); @@ -43,12 +44,6 @@ jest.mock('@tidepool/viz', () => { }; }); -jest.mock('@app/providers/ToastProvider', () => ({ - useToasts: jest.fn().mockReturnValue({ - set: jest.fn(), - }), -})); - const mockStore = configureStore([thunk]); const defaultProps = { @@ -67,13 +62,6 @@ const defaultPatient = { const defaultState = { blip: { selectedClinicId: 'clinic123', - clinics: { - clinic123: { - patients: { - patient123: defaultPatient, - }, - }, - }, pdf: { data: { agpCGM: { @@ -110,12 +98,17 @@ const renderMenuBar = (props = {}, state = defaultState) => { describe('MenuBar Component', () => { beforeEach(() => { jest.clearAllMocks(); + useGetPatientDrawerPatientQuery.mockReturnValue({ data: defaultPatient }); }); describe('Patient Information Display', () => { it('should display patient name when patient data is available', () => { renderMenuBar(); + expect(useGetPatientDrawerPatientQuery).toHaveBeenCalledWith( + { clinicId: 'clinic123', patientId: 'patient123' }, + { skip: false }, + ); expect(screen.getByTestId('patient-name')).toHaveTextContent('John Doe'); }); @@ -126,23 +119,9 @@ describe('MenuBar Component', () => { }); it('should handle missing patient birthdate gracefully', () => { - const stateWithoutBirthdate = { - ...defaultState, - blip: { - ...defaultState.blip, - clinics: { - clinic123: { - patients: { - patient123: { - fullName: 'John Doe', - }, - }, - }, - }, - }, - }; + useGetPatientDrawerPatientQuery.mockReturnValue({ data: { fullName: 'John Doe' } }); - renderMenuBar({}, stateWithoutBirthdate); + renderMenuBar(); expect(screen.getByTestId('patient-name')).toHaveTextContent('John Doe'); expect(screen.queryByTestId('patient-birthdate')).not.toBeInTheDocument(); @@ -154,7 +133,7 @@ describe('MenuBar Component', () => { renderMenuBar({}, defaultState); expect(screen.getByTestId('last-reviewed-section')).toBeInTheDocument(); - expect(screen.getByTestId('patient-review-toggle')).toBeInTheDocument(); + expect(screen.getByTestId('patient-last-reviewed')).toBeInTheDocument(); expect(screen.getByText('Last Reviewed')).toBeInTheDocument(); }); }); diff --git a/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js b/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js index 209f6a609b..66578c0ff5 100644 --- a/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js +++ b/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js @@ -8,8 +8,8 @@ import { MemoryRouter } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { thunk } from 'redux-thunk'; -import StackedDaily from '@app/pages/dashboard/PatientDrawer/StackedDaily'; -import { STATUS } from '@app/pages/dashboard/PatientDrawer/useAgpCGM'; +import StackedDaily from '@app/components/PatientDrawer/StackedDaily'; +import { STATUS } from '@app/components/PatientDrawer/useAgpCGM'; import { mean } from 'lodash'; import { MS_IN_DAY } from '../../../../../../app/core/constants'; @@ -58,7 +58,7 @@ jest.mock('@tidepool/viz', () => { }); // Mock Overview components -jest.mock('@app/pages/dashboard/PatientDrawer/Overview', () => ({ +jest.mock('@app/components/PatientDrawer/Overview', () => ({ NoPatientData: function MockNoPatientData({ patientName }) { return
No data for {patientName}
; }, diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index 33b0ca8257..06d7adfd11 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -1,13 +1,11 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import * as actions from '../../../redux/actions'; import { useSelector, useDispatch } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { push } from 'connected-react-router'; import { Flex, Box, Text } from 'theme-ui'; import { colors as vizColors } from '@tidepool/viz'; import Button from '../../../components/elements/Button'; -import moment from 'moment'; import PatientLastReviewed from '../../../clinicworkspace/components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; import CGMClipboardButton from './CGMClipboardButton'; import { useGetPatientDrawerPatientQuery } from '../patientDrawerApi'; diff --git a/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js b/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js index e968196232..b935a4806f 100644 --- a/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js +++ b/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js @@ -23,7 +23,7 @@ jest.mock('@tidepool/viz', () => { }; }); -import CGMClipboardButton from '../../../../../../app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton'; +import CGMClipboardButton from '@app/components/PatientDrawer/MenuBar/CGMClipboardButton'; const patient = { birthDate: '2001-01-01', From d5cae074da40edea0cad9bc46fb044f87415c408 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 15:13:48 -0700 Subject: [PATCH 102/123] WEB-4460 menubar uses generic `PatientLastReviewed` adapter --- .../MenuBar/PatientLastReviewed.js | 65 ------------------- app/components/clinic/PatientLastReviewed.js | 1 + 2 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 app/components/PatientDrawer/MenuBar/PatientLastReviewed.js diff --git a/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js b/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js deleted file mode 100644 index 8bf496fb77..0000000000 --- a/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; -import moment from 'moment-timezone'; -import noop from 'lodash/noop'; - -import ReviewPatientToggle, { - useMarkPatientReviewedMutation, - useUndoPatientReviewedMutation, -} from '../../../pages/clinicworkspace/components/ReviewPatientToggle'; - -import * as ErrorMessages from '../../../redux/constants/errorMessages'; -import { useToasts } from '../../../providers/ToastProvider'; - -const PatientLastReviewed = ({ patient, onReview = noop }) => { - const { set: setToast } = useToasts(); - const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); - const patientId = patient?.id; - - const recentlyReviewedThresholdDate = moment().startOf('isoWeek').toISOString(); - - const [reviews, setReviews] = useState(patient?.reviews || []); - - useEffect(() => { // Sync up if prop updates - setReviews(patient?.reviews || []); - }, [patientId, patient?.reviews]); - - const [markPatientReviewed, { isLoading: isMarking }] = useMarkPatientReviewedMutation(); - const [undoPatientReviewed, { isLoading: isUndoing }] = useUndoPatientReviewedMutation(); - - const handleReview = () => { - markPatientReviewed({ clinicId: selectedClinicId, patientId }) - .unwrap() - .then(updatedReviews => { - setReviews(updatedReviews || []); - onReview(); - }) - .catch(() => setToast({ message: ErrorMessages.ERR_SETTING_CLINIC_PATIENT_LAST_REVIEWED, variant: 'danger' })); - }; - - const handleUndo = () => { - undoPatientReviewed({ clinicId: selectedClinicId, patientId }) - .unwrap() - .then(updatedReviews => setReviews(updatedReviews || [])) - .catch(() => setToast({ message: ErrorMessages.ERR_REVERTING_CLINIC_PATIENT_LAST_REVIEWED, variant: 'danger' })); - }; - - return ( - - ); -}; - -PatientLastReviewed.propTypes = { - patient: PropTypes.object, - onReview: PropTypes.func, -}; - -export default PatientLastReviewed; diff --git a/app/components/clinic/PatientLastReviewed.js b/app/components/clinic/PatientLastReviewed.js index 5859fbee95..4e30bc87fe 100644 --- a/app/components/clinic/PatientLastReviewed.js +++ b/app/components/clinic/PatientLastReviewed.js @@ -79,6 +79,7 @@ export const PatientLastReviewed = ({ api, patientId }) => { PatientLastReviewed.propTypes = { api: PropTypes.object.isRequired, patientId: PropTypes.string.isRequired, + onReview: PropTypes.func, }; export default PatientLastReviewed; From 73314b9db0789dd27f26eda0e8f88ee3cb67f4fd Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 15:27:39 -0700 Subject: [PATCH 103/123] WEB-4460 update tests for MenuBar --- .../PatientDrawer/MenuBar/MenuBar.test.js | 70 +++++++++++-------- .../PatientDrawer/MenuBar/MenuBar.js | 2 +- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js index 9f62196b3c..12e310cdf4 100644 --- a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js +++ b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js @@ -1,17 +1,18 @@ -/* global jest, beforeEach, afterEach, test, expect, describe, it */ +/* global jest, beforeAll, beforeEach, afterEach, afterAll, expect, describe, it */ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; -import configureStore from 'redux-mock-store'; -import { thunk } from 'redux-thunk'; import { I18nextProvider } from 'react-i18next'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; import { trackMetric as mockTrackMetric } from '../../../../../../app/core/metricUtils'; +import { setupStore } from '@tests/utils/setupStore'; +import blipReducer from '@app/redux/reducers'; import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from '@app/components/PatientDrawer/MenuBar/MenuBar'; -import { useGetPatientDrawerPatientQuery } from '@app/components/PatientDrawer/patientDrawerApi'; import i18n from '@app/core/language'; // Mock actions @@ -44,7 +45,14 @@ jest.mock('@tidepool/viz', () => { }; }); -const mockStore = configureStore([thunk]); +const TEST_TIMEOUT_MS = 30_000; + +const patientUrl = 'http://app.tidepool.test/v1/clinics/clinic123/patients/patient123'; +const defaultPatient = { id: 'patient123', fullName: 'Zhilei Zhang', birthDate: '1990-01-15' }; + +const server = setupServer( + http.get(patientUrl, () => HttpResponse.json(defaultPatient)), +); const defaultProps = { patientId: 'patient123', @@ -53,12 +61,6 @@ const defaultProps = { selectedTab: OVERVIEW_TAB_INDEX, }; -const defaultPatient = { - id: 'patient123', - fullName: 'John Doe', - birthDate: '1990-01-15', -}; - const defaultState = { blip: { selectedClinicId: 'clinic123', @@ -82,7 +84,7 @@ const defaultState = { }; const renderMenuBar = (props = {}, state = defaultState) => { - const store = mockStore(state); + const store = setupStore(state, { blip: blipReducer }); return render( @@ -96,46 +98,52 @@ const renderMenuBar = (props = {}, state = defaultState) => { }; describe('MenuBar Component', () => { + beforeAll(() => server.listen()); + beforeEach(() => { jest.clearAllMocks(); - useGetPatientDrawerPatientQuery.mockReturnValue({ data: defaultPatient }); }); + afterEach(() => server.resetHandlers()); + + afterAll(() => server.close()); + describe('Patient Information Display', () => { - it('should display patient name when patient data is available', () => { + it('should display patient name when patient data is available', async () => { renderMenuBar(); - expect(useGetPatientDrawerPatientQuery).toHaveBeenCalledWith( - { clinicId: 'clinic123', patientId: 'patient123' }, - { skip: false }, - ); - expect(screen.getByTestId('patient-name')).toHaveTextContent('John Doe'); - }); + expect(screen.getByTestId('patient-name')).toBeEmptyDOMElement(); + + expect(await screen.findByText('Zhilei Zhang')).toBeInTheDocument(); + expect(screen.getByTestId('patient-name')).toHaveTextContent('Zhilei Zhang'); + }, TEST_TIMEOUT_MS); - it('should display patient birthdate when patient data is available', () => { + it('should display patient birthdate when patient data is available', async () => { renderMenuBar(); - expect(screen.getByTestId('patient-birthdate')).toHaveTextContent('DOB: 1990-01-15'); - }); + expect(await screen.findByTestId('patient-birthdate')).toHaveTextContent('DOB: 1990-01-15'); + }, TEST_TIMEOUT_MS); - it('should handle missing patient birthdate gracefully', () => { - useGetPatientDrawerPatientQuery.mockReturnValue({ data: { fullName: 'John Doe' } }); + it('should handle missing patient birthdate gracefully', async () => { + server.use( + http.get(patientUrl, () => HttpResponse.json({ fullName: 'Zhilei Zhang' })), + ); renderMenuBar(); - expect(screen.getByTestId('patient-name')).toHaveTextContent('John Doe'); + expect(await screen.findByText('Zhilei Zhang')).toBeInTheDocument(); expect(screen.queryByTestId('patient-birthdate')).not.toBeInTheDocument(); - }); + }, TEST_TIMEOUT_MS); }); describe('Last Reviewed Component', () => { it('should display last reviewed section', () => { - renderMenuBar({}, defaultState); + renderMenuBar(); expect(screen.getByTestId('last-reviewed-section')).toBeInTheDocument(); expect(screen.getByTestId('patient-last-reviewed')).toBeInTheDocument(); expect(screen.getByText('Last Reviewed')).toBeInTheDocument(); - }); + }, TEST_TIMEOUT_MS); }); describe('Tab Navigation', () => { @@ -186,7 +194,7 @@ describe('MenuBar Component', () => { }); describe('View Data Button', () => { - it('should render view data button', () => { + it('should render view data button', async () => { renderMenuBar(); const viewDataButton = screen.getByTestId('view-data-button'); @@ -294,6 +302,6 @@ describe('MenuBar Component', () => { const cgmButton = screen.getByTestId('cgm-clipboard-button'); expect(cgmButton).toBeDisabled(); - }); + }, TEST_TIMEOUT_MS); }); }); diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index 06d7adfd11..00114f7fab 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -89,7 +89,7 @@ const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => { {t('Last Reviewed')} - + {!!patient && }
From 4f992d7229742b79c4d2ec93418edfc739218f8b Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 17:40:19 -0700 Subject: [PATCH 104/123] WEB-4460 fix failing tests --- .../PatientDrawer/MenuBar/MenuBar.test.js | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js index 12e310cdf4..1cc44f6dbf 100644 --- a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js +++ b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js @@ -15,20 +15,8 @@ import blipReducer from '@app/redux/reducers'; import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from '@app/components/PatientDrawer/MenuBar/MenuBar'; import i18n from '@app/core/language'; -// Mock actions -jest.mock('@app/redux/actions', () => ({})); - // Mock connected-react-router -jest.mock('connected-react-router', () => ({ - push: jest.fn(), -})); - -// Mock api -jest.mock('@app/core/api', () => ({})); - -jest.mock('@app/components/PatientDrawer/patientDrawerApi', () => ({ - useGetPatientDrawerPatientQuery: jest.fn(), -})); +jest.mock('connected-react-router', () => ({ push: jest.fn() })); // Mock the agpCGMText function from @tidepool/viz while preserving other exports jest.mock('@tidepool/viz', () => { @@ -45,13 +33,16 @@ jest.mock('@tidepool/viz', () => { }; }); +jest.mock('@app/providers/ToastProvider', () => ({ + useToasts: () => ({ set: jest.fn() }), +})); + const TEST_TIMEOUT_MS = 30_000; const patientUrl = 'http://app.tidepool.test/v1/clinics/clinic123/patients/patient123'; -const defaultPatient = { id: 'patient123', fullName: 'Zhilei Zhang', birthDate: '1990-01-15' }; const server = setupServer( - http.get(patientUrl, () => HttpResponse.json(defaultPatient)), + http.get(patientUrl, () => HttpResponse.json({ id: 'patient123', fullName: 'Zhilei Zhang', birthDate: '1990-01-15' })), ); const defaultProps = { @@ -137,11 +128,11 @@ describe('MenuBar Component', () => { }); describe('Last Reviewed Component', () => { - it('should display last reviewed section', () => { + it('should display last reviewed section', async () => { renderMenuBar(); expect(screen.getByTestId('last-reviewed-section')).toBeInTheDocument(); - expect(screen.getByTestId('patient-last-reviewed')).toBeInTheDocument(); + expect(await screen.findByTestId('patient-review-toggle')).toBeInTheDocument(); // shows on patient fetch expect(screen.getByText('Last Reviewed')).toBeInTheDocument(); }, TEST_TIMEOUT_MS); }); From a5aa1e1c78aa439280972e9265caf64846be96be Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 10:30:07 -0700 Subject: [PATCH 105/123] WEB-4460 fix breaking drawer due to patient not being in redux state --- .../PatientDrawer/MenuBar/MenuBar.js | 9 ++------ app/components/PatientDrawer/Overview.js | 4 +--- app/components/PatientDrawer/PatientDrawer.js | 23 ++++++++++++++----- app/components/PatientDrawer/StackedDaily.js | 4 +--- .../PatientDrawer/useAgpCGM/useAgpCGM.js | 4 ++-- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index 00114f7fab..403b1e0b44 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -30,17 +30,14 @@ const tabs = { }, }; -const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => { +const MenuBar = ({ patient, onClose, onSelectTab, selectedTab }) => { const dispatch = useDispatch(); const { t } = useTranslation(); const selectedClinicId = useSelector(state => state.blip.selectedClinicId); const pdf = useSelector(state => state.blip.pdf); // IMPORTANT: Data taken from Redux PDF slice - const { data: patient } = useGetPatientDrawerPatientQuery( - { clinicId: selectedClinicId, patientId }, - { skip: !selectedClinicId || !patientId } - ); + const { id: patientId, fullName, birthDate } = patient || {}; const handleViewData = () => { dispatch(push(`/patients/${patientId}/data/trends?dashboard=tide&drawerTab=${selectedTab}`)); @@ -58,8 +55,6 @@ const MenuBar = ({ patientId, onClose, onSelectTab, selectedTab }) => { onSelectTab(tabIndex); } - const { fullName, birthDate } = patient || {}; - return ( diff --git a/app/components/PatientDrawer/Overview.js b/app/components/PatientDrawer/Overview.js index c103fb86ac..e2bcb7e197 100644 --- a/app/components/PatientDrawer/Overview.js +++ b/app/components/PatientDrawer/Overview.js @@ -53,11 +53,9 @@ const CategoryContainer = ({ title, subtitle, children }) => { ); }; -const Overview = ({ patientId, agpCGMData }) => { +const Overview = ({ patient, agpCGMData }) => { const { t } = useTranslation(); const { status, svgDataURLS, agpCGM, offsetAgpCGM } = agpCGMData; - const clinic = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]); - const patient = clinic?.patients?.[patientId]; if (status === STATUS.NO_PATIENT_DATA) return ; if (status === STATUS.INSUFFICIENT_DATA) return ; diff --git a/app/components/PatientDrawer/PatientDrawer.js b/app/components/PatientDrawer/PatientDrawer.js index 60a1bea5da..b9d7289acc 100644 --- a/app/components/PatientDrawer/PatientDrawer.js +++ b/app/components/PatientDrawer/PatientDrawer.js @@ -13,6 +13,8 @@ import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from './MenuBar' import useAgpCGM from './useAgpCGM'; import { shadows } from '../../themes/baseTheme'; import { useScrollToTop } from '../../core/hooks'; +import { useGetPatientDrawerPatientQuery } from './patientDrawerApi'; +import { useSelector } from 'react-redux'; const StyledCloseButton = styled(Icon)` position: absolute; @@ -52,7 +54,7 @@ const getAgpPeriodInDays = (period) => { } }; -const DrawerContent = ({ patientId, onClose, api, period }) => { +const DrawerContent = ({ patient, onClose, api, period }) => { // Only rendered when patient is selected and isOpen is true // this will also allow the hook to dismount and for the cleanup to be called const location = useLocation(); @@ -61,7 +63,7 @@ const DrawerContent = ({ patientId, onClose, api, period }) => { const [selectedTab, setSelectedTab] = React.useState(drawerTab); const [scrolledToTop, setScrolledToTop] = React.useState(true); const agpPeriodInDays = getAgpPeriodInDays(period); - const agpCGMData = useAgpCGM(api, patientId, agpPeriodInDays); + const agpCGMData = useAgpCGM(api, patient, agpPeriodInDays); const contentRef = React.useRef(undefined); useScrollToTop(contentRef?.current, [selectedTab]); @@ -84,7 +86,7 @@ const DrawerContent = ({ patientId, onClose, api, period }) => { return ( <> - + @@ -100,8 +102,8 @@ const DrawerContent = ({ patientId, onClose, api, period }) => { onScroll={handleContentScroll} ref={contentRef} > - {selectedTab === OVERVIEW_TAB_INDEX && } - {selectedTab === STACKED_DAILY_TAB_INDEX && } + {selectedTab === OVERVIEW_TAB_INDEX && } + {selectedTab === STACKED_DAILY_TAB_INDEX && } ) @@ -111,6 +113,15 @@ const PatientDrawer = ({ patientId, onClose, api, period }) => { const classes = useStyles(); const isOpen = !!patientId && isValidAgpPeriod(period); + const selectedClinicId = useSelector(state => state.blip.selectedClinicId); + + const { data: patient } = useGetPatientDrawerPatientQuery( + { clinicId: selectedClinicId, patientId }, + { skip: !selectedClinicId || !patientId } + ); + + const showContent = isOpen && !!patient; + return ( { flexDirection: 'column', }} > - {isOpen && } + {showContent && } ); diff --git a/app/components/PatientDrawer/StackedDaily.js b/app/components/PatientDrawer/StackedDaily.js index d09fab05ea..a1d9bbeb56 100644 --- a/app/components/PatientDrawer/StackedDaily.js +++ b/app/components/PatientDrawer/StackedDaily.js @@ -25,15 +25,13 @@ import BgLegend from '../../components/chart/BgLegend'; const CHART_HEIGHT = 200; -const StackedDaily = ({ patientId, agpCGMData }) => { +const StackedDaily = ({ patient, agpCGMData }) => { const { t } = useTranslation(); const { status } = agpCGMData; const chartRefs = useRef([]); const containerRef = useRef(null); const [hoveredSMBG, setHoveredSMBG] = React.useState(false); const [hoveredCBG, setHoveredCBG] = React.useState(false); - const clinic = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]); - const patient = clinic?.patients?.[patientId]; const dispatch = useDispatch(); const bgPrefs = agpCGMData?.agpCGM?.query?.bgPrefs; const bgClasses = bgPrefs?.bgClasses; diff --git a/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js index e72f0a6dfa..7500b7c42a 100644 --- a/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js +++ b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js @@ -74,7 +74,7 @@ const DEFAULT_AGP_PERIOD_IN_DAYS = 14; const useAgpCGM = ( api, - patientId, + clinicPatient, agpPeriodInDays = DEFAULT_AGP_PERIOD_IN_DAYS, ) => { const dispatch = useDispatch(); @@ -84,7 +84,7 @@ const useAgpCGM = ( const data = useSelector(state => state.blip.data); const pdf = useSelector(state => state.blip.pdf); const clinic = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]); - const clinicPatient = clinic?.patients?.[patientId]; + const patientId = clinicPatient?.id; const lastCompletedStep = inferLastCompletedStep(requestId, patientId, data, pdf); From ffe084c06807eba602ba5a26a02f316b02be140e Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 11:39:38 -0700 Subject: [PATCH 106/123] WEB-4460 fix tests for drawer components --- .../dashboard/PatientDrawer/Overview.test.js | 26 +++------ .../PatientDrawer/useAgpCGM/useAgpCGM.test.js | 12 ++--- .../PatientDrawer/MenuBar/MenuBar.test.js | 54 +++++-------------- .../StackedDaily/StackedDaily.test.js | 15 +----- .../PatientDrawer/MenuBar/MenuBar.js | 3 +- app/components/PatientDrawer/Overview.js | 1 - app/components/PatientDrawer/PatientDrawer.js | 4 +- app/components/PatientDrawer/StackedDaily.js | 3 +- test/unit/pages/TideDashboard.test.js | 5 ++ 9 files changed, 37 insertions(+), 86 deletions(-) diff --git a/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js b/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js index fb88fc4d6e..537fb283a9 100644 --- a/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js +++ b/__tests__/unit/app/pages/dashboard/PatientDrawer/Overview.test.js @@ -5,35 +5,21 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import _ from 'lodash'; -import configureStore from 'redux-mock-store'; -import { Provider } from 'react-redux'; -import { thunk } from 'redux-thunk'; import Overview from '@app/components/PatientDrawer/Overview'; import { STATUS } from '@app/components/PatientDrawer/useAgpCGM'; -const mockStore = configureStore([thunk]); - describe('PatientDrawer/Overview', () => { - const store = mockStore({ - blip: { - selectedClinicId: '5678-efgh', - clinics: { '5678-efgh': { patients: { '1234-abcd': { fullName: 'Naoya Inoue' } } } }, - }, - }); - const props = { - api: { foo: 'bar' }, - patientId: '1234-abcd', + patient: { id: '1234-abcd', fullName: 'Naoya Inoue' }, }; describe('When patient has no data in the platform', () => { it('shows no data fields and an appropriate message to the user', () => { const agpCGMData = { status: STATUS.NO_PATIENT_DATA }; - render( ); + render(); expect(screen.getByText('Naoya Inoue does not have any data yet.')).toBeInTheDocument(); expect(screen.queryByText('Time in Ranges')).not.toBeInTheDocument(); @@ -46,7 +32,7 @@ describe('PatientDrawer/Overview', () => { it('shows a message about data being insufficient', () => { const agpCGMData = { status: STATUS.INSUFFICIENT_DATA }; - render( ); + render(); expect(screen.getByText('Insufficient data to generate AGP Report.')).toBeInTheDocument(); expect(screen.queryByText('Time in Ranges')).not.toBeInTheDocument(); @@ -59,7 +45,7 @@ describe('PatientDrawer/Overview', () => { it('shows a loader', () => { const agpCGMData = { status: STATUS.PATIENT_LOADED }; // any intermediate state prior to 'SVGS_GENERATED' - render( ); + render(); const loader = document.getElementsByClassName('loader')?.[0]; //eslint-disable-line expect(loader).toBeTruthy(); @@ -90,7 +76,7 @@ describe('PatientDrawer/Overview', () => { }, }; - render( ); + render(); expect(screen.getByText('Time in Ranges')).toBeInTheDocument(); expect(screen.getByText('Ambulatory Glucose Profile (AGP)')).toBeInTheDocument(); @@ -127,7 +113,7 @@ describe('PatientDrawer/Overview', () => { }, }; - render( ); + render(); expect(screen.getByText('Insufficient CGM data to generate AGP graph')).toBeInTheDocument(); diff --git a/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js b/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js index 2a14fd7cda..f753f90ca8 100644 --- a/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js +++ b/__tests__/unit/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.test.js @@ -74,7 +74,7 @@ const working = { describe('useAgpCGM', () => { const getWrapper = (store) => ({ children }) => {children}; - const patientId = 'patient-1'; + const patient = { id: 'patient-1', fullName: 'Naoya Inoue' }; const api = { foo: 'bar' }; actions.worker.removeGeneratedPDFS.mockReturnValue({ type: 'MOCK_ACTION' }); @@ -99,7 +99,7 @@ describe('useAgpCGM', () => { const wrapper = getWrapper(store); it('returns correct status and begins state cleanup', () => { - const { result } = renderHook(() => useAgpCGM(api, patientId), { wrapper }); + const { result } = renderHook(() => useAgpCGM(api, patient), { wrapper }); expect(actions.worker.removeGeneratedPDFS).toHaveBeenCalledTimes(1); expect(actions.worker.dataWorkerRemoveDataRequest).toHaveBeenCalledTimes(1); @@ -122,7 +122,7 @@ describe('useAgpCGM', () => { const wrapper = getWrapper(store); it('returns correct status and begins patient fetch', () => { - const { result } = renderHook(() => useAgpCGM(api, patientId), { wrapper }); + const { result } = renderHook(() => useAgpCGM(api, patient), { wrapper }); expect(actions.async.fetchPatientData).toHaveBeenCalledTimes(1); expect(result.current).toStrictEqual({ status: 'STATE_CLEARED', svgDataURLS: null, agpCGM: null, offsetAgpCGM: null }); @@ -144,7 +144,7 @@ describe('useAgpCGM', () => { const wrapper = getWrapper(store); it('returns correct status and begins PDF generation', () => { - const { result } = renderHook(() => useAgpCGM(api, patientId), { wrapper }); + const { result } = renderHook(() => useAgpCGM(api, patient), { wrapper }); expect(actions.worker.generatePDFRequest).toHaveBeenCalledTimes(1); expect(result.current).toStrictEqual({ status: 'PATIENT_LOADED', svgDataURLS: null, agpCGM: null, offsetAgpCGM: null }); @@ -172,7 +172,7 @@ describe('useAgpCGM', () => { const wrapper = getWrapper(store); it('returns correct status and begins AGP image generation', () => { - const { result } = renderHook(() => useAgpCGM(api, patientId), { wrapper }); + const { result } = renderHook(() => useAgpCGM(api, patient), { wrapper }); expect(vizUtils.agp.generateAGPFigureDefinitions).toHaveBeenCalledTimes(1); expect(result.current).toStrictEqual({ @@ -208,7 +208,7 @@ describe('useAgpCGM', () => { const wrapper = getWrapper(store); it('returns correct status and returns data', () => { - const { result } = renderHook(() => useAgpCGM(api, patientId), { wrapper }); + const { result } = renderHook(() => useAgpCGM(api, patient), { wrapper }); expect(result.current).toStrictEqual({ status: 'SVGS_GENERATED', diff --git a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js index 1cc44f6dbf..5fb9c825dc 100644 --- a/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js +++ b/__tests__/unit/pages/dashboard/PatientDrawer/MenuBar/MenuBar.test.js @@ -1,4 +1,4 @@ -/* global jest, beforeAll, beforeEach, afterEach, afterAll, expect, describe, it */ +/* global jest, beforeEach, expect, describe, it */ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; @@ -6,8 +6,6 @@ import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { I18nextProvider } from 'react-i18next'; -import { http, HttpResponse } from 'msw'; -import { setupServer } from 'msw/node'; import { trackMetric as mockTrackMetric } from '../../../../../../app/core/metricUtils'; import { setupStore } from '@tests/utils/setupStore'; @@ -37,16 +35,8 @@ jest.mock('@app/providers/ToastProvider', () => ({ useToasts: () => ({ set: jest.fn() }), })); -const TEST_TIMEOUT_MS = 30_000; - -const patientUrl = 'http://app.tidepool.test/v1/clinics/clinic123/patients/patient123'; - -const server = setupServer( - http.get(patientUrl, () => HttpResponse.json({ id: 'patient123', fullName: 'Zhilei Zhang', birthDate: '1990-01-15' })), -); - const defaultProps = { - patientId: 'patient123', + patient: { id: 'patient123', fullName: 'Zhilei Zhang', birthDate: '1990-01-15' }, onClose: jest.fn(), onSelectTab: jest.fn(), selectedTab: OVERVIEW_TAB_INDEX, @@ -89,52 +79,34 @@ const renderMenuBar = (props = {}, state = defaultState) => { }; describe('MenuBar Component', () => { - beforeAll(() => server.listen()); - beforeEach(() => { jest.clearAllMocks(); }); - afterEach(() => server.resetHandlers()); - - afterAll(() => server.close()); - describe('Patient Information Display', () => { - it('should display patient name when patient data is available', async () => { + it('should display patient name and birthdate', () => { renderMenuBar(); - expect(screen.getByTestId('patient-name')).toBeEmptyDOMElement(); - - expect(await screen.findByText('Zhilei Zhang')).toBeInTheDocument(); expect(screen.getByTestId('patient-name')).toHaveTextContent('Zhilei Zhang'); - }, TEST_TIMEOUT_MS); - - it('should display patient birthdate when patient data is available', async () => { - renderMenuBar(); - - expect(await screen.findByTestId('patient-birthdate')).toHaveTextContent('DOB: 1990-01-15'); - }, TEST_TIMEOUT_MS); - - it('should handle missing patient birthdate gracefully', async () => { - server.use( - http.get(patientUrl, () => HttpResponse.json({ fullName: 'Zhilei Zhang' })), - ); + expect(screen.getByTestId('patient-birthdate')).toHaveTextContent('DOB: 1990-01-15'); + }); - renderMenuBar(); + it('should handle missing patient birthdate gracefully', () => { + renderMenuBar({ patient: { id: 'patient123', fullName: 'Zhilei Zhang' } }); - expect(await screen.findByText('Zhilei Zhang')).toBeInTheDocument(); + expect(screen.getByText('Zhilei Zhang')).toBeInTheDocument(); expect(screen.queryByTestId('patient-birthdate')).not.toBeInTheDocument(); - }, TEST_TIMEOUT_MS); + }); }); describe('Last Reviewed Component', () => { - it('should display last reviewed section', async () => { + it('should display last reviewed section', () => { renderMenuBar(); expect(screen.getByTestId('last-reviewed-section')).toBeInTheDocument(); - expect(await screen.findByTestId('patient-review-toggle')).toBeInTheDocument(); // shows on patient fetch + expect(screen.getByTestId('patient-review-toggle')).toBeInTheDocument(); expect(screen.getByText('Last Reviewed')).toBeInTheDocument(); - }, TEST_TIMEOUT_MS); + }); }); describe('Tab Navigation', () => { @@ -293,6 +265,6 @@ describe('MenuBar Component', () => { const cgmButton = screen.getByTestId('cgm-clipboard-button'); expect(cgmButton).toBeDisabled(); - }, TEST_TIMEOUT_MS); + }); }); }); diff --git a/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js b/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js index 66578c0ff5..7e60869117 100644 --- a/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js +++ b/__tests__/unit/pages/dashboard/PatientDrawer/StackedDaily/StackedDaily.test.js @@ -77,7 +77,7 @@ jest.mock('@app/components/chart/BgLegend', () => { const mockStore = configureStore([thunk]); const defaultProps = { - patientId: 'patient123', + patient: { id: 'patient123', fullName: 'John Doe' }, agpCGMData: { status: STATUS.DATA_PROCESSED, agpCGM: { @@ -107,18 +107,7 @@ const defaultProps = { }; const defaultState = { - blip: { - selectedClinicId: 'clinic123', - clinics: { - clinic123: { - patients: { - patient123: { - fullName: 'John Doe', - }, - }, - }, - }, - }, + blip: {}, }; const renderComponent = (props = {}, storeState = {}) => { diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index 403b1e0b44..c7343485c3 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -8,7 +8,6 @@ import { colors as vizColors } from '@tidepool/viz'; import Button from '../../../components/elements/Button'; import PatientLastReviewed from '../../../clinicworkspace/components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; import CGMClipboardButton from './CGMClipboardButton'; -import { useGetPatientDrawerPatientQuery } from '../patientDrawerApi'; import { map, keys } from 'lodash'; import copyIcon from '../../../core/icons/copyIcon.svg'; import viewIcon from '../../../core/icons/viewIcon.svg'; @@ -132,7 +131,7 @@ const MenuBar = ({ patient, onClose, onSelectTab, selectedTab }) => { } MenuBar.propTypes = { - patientId: PropTypes.string.isRequired, + patient: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, onSelectTab: PropTypes.func.isRequired, selectedTab: PropTypes.number.isRequired, diff --git a/app/components/PatientDrawer/Overview.js b/app/components/PatientDrawer/Overview.js index e2bcb7e197..a04374a904 100644 --- a/app/components/PatientDrawer/Overview.js +++ b/app/components/PatientDrawer/Overview.js @@ -1,5 +1,4 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Flex, Box, Text } from 'theme-ui'; import { colors as vizColors } from '@tidepool/viz'; diff --git a/app/components/PatientDrawer/PatientDrawer.js b/app/components/PatientDrawer/PatientDrawer.js index b9d7289acc..569a3c3cff 100644 --- a/app/components/PatientDrawer/PatientDrawer.js +++ b/app/components/PatientDrawer/PatientDrawer.js @@ -115,7 +115,7 @@ const PatientDrawer = ({ patientId, onClose, api, period }) => { const selectedClinicId = useSelector(state => state.blip.selectedClinicId); - const { data: patient } = useGetPatientDrawerPatientQuery( + const { currentData: patient } = useGetPatientDrawerPatientQuery( { clinicId: selectedClinicId, patientId }, { skip: !selectedClinicId || !patientId } ); @@ -140,7 +140,7 @@ const PatientDrawer = ({ patientId, onClose, api, period }) => { flexDirection: 'column', }} > - {showContent && } + { showContent && } ); diff --git a/app/components/PatientDrawer/StackedDaily.js b/app/components/PatientDrawer/StackedDaily.js index a1d9bbeb56..ad6504ce66 100644 --- a/app/components/PatientDrawer/StackedDaily.js +++ b/app/components/PatientDrawer/StackedDaily.js @@ -1,5 +1,5 @@ import React, { useRef, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { push } from 'connected-react-router'; import { useTranslation } from 'react-i18next'; import { Flex, Box } from 'theme-ui'; @@ -27,6 +27,7 @@ const CHART_HEIGHT = 200; const StackedDaily = ({ patient, agpCGMData }) => { const { t } = useTranslation(); + const { id: patientId } = patient || {}; const { status } = agpCGMData; const chartRefs = useRef([]); const containerRef = useRef(null); diff --git a/test/unit/pages/TideDashboard.test.js b/test/unit/pages/TideDashboard.test.js index 18ba059430..fa026ed4ce 100644 --- a/test/unit/pages/TideDashboard.test.js +++ b/test/unit/pages/TideDashboard.test.js @@ -66,6 +66,11 @@ jest.mock('../../../app/core/hooks', () => { }; }); +jest.mock('../../../app/components/PatientDrawer/patientDrawerApi', () => ({ + ...jest.requireActual('../../../app/components/PatientDrawer/patientDrawerApi'), + useGetPatientDrawerPatientQuery: jest.fn(() => ({ currentData: undefined })), +})); + jest.mock('../../../app/components/clinic/PatientForm/SelectTags', () => { const React = require('react'); return jest.fn((props) => ( From 97d39c1419ea16f02486fb3ce4ed72a3c57f3396 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 15:04:28 -0700 Subject: [PATCH 107/123] WEB-4460 fix PatientLastReviewed import with generic adapter --- app/components/PatientDrawer/MenuBar/MenuBar.js | 2 +- .../PatientDrawer/MenuBar/PatientLastReviewed.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 app/components/PatientDrawer/MenuBar/PatientLastReviewed.js diff --git a/app/components/PatientDrawer/MenuBar/MenuBar.js b/app/components/PatientDrawer/MenuBar/MenuBar.js index c7343485c3..d5b3058a62 100644 --- a/app/components/PatientDrawer/MenuBar/MenuBar.js +++ b/app/components/PatientDrawer/MenuBar/MenuBar.js @@ -6,7 +6,7 @@ import { push } from 'connected-react-router'; import { Flex, Box, Text } from 'theme-ui'; import { colors as vizColors } from '@tidepool/viz'; import Button from '../../../components/elements/Button'; -import PatientLastReviewed from '../../../clinicworkspace/components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; +import PatientLastReviewed from './PatientLastReviewed'; import CGMClipboardButton from './CGMClipboardButton'; import { map, keys } from 'lodash'; import copyIcon from '../../../core/icons/copyIcon.svg'; diff --git a/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js b/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js new file mode 100644 index 0000000000..0bc4b05844 --- /dev/null +++ b/app/components/PatientDrawer/MenuBar/PatientLastReviewed.js @@ -0,0 +1,8 @@ +import React from 'react'; +import PatientLastReviewedGenericAdapter from '../../../pages/clinicworkspace/components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; + +const PatientLastReviewed = (props) => { + return ; +}; + +export default PatientLastReviewed; From ccbeaf17a697cc97729ff9ab920f1f8c282d20bf Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 15:41:02 -0700 Subject: [PATCH 108/123] WEB-4460 remove unneeded Drawer Api --- app/components/PatientDrawer/PatientDrawer.js | 17 +++--------- .../PatientDrawer/patientDrawerApi.js | 26 ------------------- .../PatientDrawerController.js | 6 +++-- .../TideDashboardV2/TideDashboardV2.js | 2 +- .../ReviewPatientToggle/reviewPatientApi.js | 6 ++--- test/unit/pages/TideDashboard.test.js | 5 ---- 6 files changed, 10 insertions(+), 52 deletions(-) delete mode 100644 app/components/PatientDrawer/patientDrawerApi.js diff --git a/app/components/PatientDrawer/PatientDrawer.js b/app/components/PatientDrawer/PatientDrawer.js index 569a3c3cff..ea1dbefce2 100644 --- a/app/components/PatientDrawer/PatientDrawer.js +++ b/app/components/PatientDrawer/PatientDrawer.js @@ -13,8 +13,6 @@ import MenuBar, { OVERVIEW_TAB_INDEX, STACKED_DAILY_TAB_INDEX } from './MenuBar' import useAgpCGM from './useAgpCGM'; import { shadows } from '../../themes/baseTheme'; import { useScrollToTop } from '../../core/hooks'; -import { useGetPatientDrawerPatientQuery } from './patientDrawerApi'; -import { useSelector } from 'react-redux'; const StyledCloseButton = styled(Icon)` position: absolute; @@ -109,18 +107,9 @@ const DrawerContent = ({ patient, onClose, api, period }) => { ) } -const PatientDrawer = ({ patientId, onClose, api, period }) => { +const PatientDrawer = ({ patient, onClose, api, period }) => { const classes = useStyles(); - const isOpen = !!patientId && isValidAgpPeriod(period); - - const selectedClinicId = useSelector(state => state.blip.selectedClinicId); - - const { currentData: patient } = useGetPatientDrawerPatientQuery( - { clinicId: selectedClinicId, patientId }, - { skip: !selectedClinicId || !patientId } - ); - - const showContent = isOpen && !!patient; + const isOpen = !!patient && isValidAgpPeriod(period); return ( { flexDirection: 'column', }} > - { showContent && } + { isOpen && } ); diff --git a/app/components/PatientDrawer/patientDrawerApi.js b/app/components/PatientDrawer/patientDrawerApi.js deleted file mode 100644 index 0863ce6e21..0000000000 --- a/app/components/PatientDrawer/patientDrawerApi.js +++ /dev/null @@ -1,26 +0,0 @@ -import { RTKQueryApi } from '../../redux/api/baseApi'; - -export const tagTypes = { - PATIENT_DRAWER_PATIENT: 'PATIENT_DRAWER_PATIENT', -}; - -const { PATIENT_DRAWER_PATIENT } = tagTypes; - -RTKQueryApi.enhanceEndpoints({ - addTagTypes: [PATIENT_DRAWER_PATIENT], -}); - -const patientDrawerApi = RTKQueryApi.injectEndpoints({ - endpoints: (builder) => ({ - getPatientDrawerPatient: builder.query({ - query: ({ clinicId, patientId }) => ({ - url: `/clinics/${clinicId}/patients/${patientId}`, - }), - providesTags: [PATIENT_DRAWER_PATIENT], - }), - }), -}); - -export const { - useGetPatientDrawerPatientQuery, -} = patientDrawerApi; diff --git a/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js b/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js index 3129a958aa..5c4a67ca81 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js +++ b/app/pages/clinicworkspace/TideDashboardV2/PatientDrawerController.js @@ -3,13 +3,15 @@ import { useSelector } from 'react-redux'; import { useLocation, useHistory } from 'react-router-dom'; import PatientDrawer from '../../../components/PatientDrawer'; -const PatientDrawerController = ({ api }) => { +const PatientDrawerController = ({ api, patients }) => { const summaryPeriod = useSelector(state => state.blip.tideDashboardFilters.summaryPeriod); const { search, pathname } = useLocation(); const history = useHistory(); const drawerPatientId = new URLSearchParams(search)?.get('drawerPatientId') || null; + const patient = patients.find(patient => patient.id === drawerPatientId); + const handleClose = () => { const params = new URLSearchParams(search); params.delete('drawerPatientId'); @@ -20,7 +22,7 @@ const PatientDrawerController = ({ api }) => { return ( diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index 97e54a4d3d..b024d6abf4 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -101,7 +101,7 @@ const TideDashboardV2 = ({ api }) => { - + diff --git a/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js b/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js index d8203ef478..cf0cac7b8d 100644 --- a/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js +++ b/app/pages/clinicworkspace/components/ReviewPatientToggle/reviewPatientApi.js @@ -1,9 +1,7 @@ import { RTKQueryApi } from '../../../../redux/api/baseApi'; import { tagTypes as tideDashboardTagTypes } from '../../TideDashboardV2/tideDashboardApi'; -import { tagTypes as patientDrawerTagTypes } from '../../../../components/PatientDrawer/patientDrawerApi'; const { TIDE_DASHBOARD_PATIENTS } = tideDashboardTagTypes; -const { PATIENT_DRAWER_PATIENT } = patientDrawerTagTypes; const reviewPatientApi = RTKQueryApi.injectEndpoints({ endpoints: (builder) => ({ @@ -12,14 +10,14 @@ const reviewPatientApi = RTKQueryApi.injectEndpoints({ url: `/clinics/${clinicId}/patients/${patientId}/reviews`, method: 'PUT', }), - invalidatesTags: [TIDE_DASHBOARD_PATIENTS, PATIENT_DRAWER_PATIENT], + invalidatesTags: [TIDE_DASHBOARD_PATIENTS], }), undoPatientReviewed: builder.mutation({ query: ({ clinicId, patientId }) => ({ url: `/clinics/${clinicId}/patients/${patientId}/reviews`, method: 'DELETE', }), - invalidatesTags: [TIDE_DASHBOARD_PATIENTS, PATIENT_DRAWER_PATIENT], + invalidatesTags: [TIDE_DASHBOARD_PATIENTS], }), }), }); diff --git a/test/unit/pages/TideDashboard.test.js b/test/unit/pages/TideDashboard.test.js index fa026ed4ce..18ba059430 100644 --- a/test/unit/pages/TideDashboard.test.js +++ b/test/unit/pages/TideDashboard.test.js @@ -66,11 +66,6 @@ jest.mock('../../../app/core/hooks', () => { }; }); -jest.mock('../../../app/components/PatientDrawer/patientDrawerApi', () => ({ - ...jest.requireActual('../../../app/components/PatientDrawer/patientDrawerApi'), - useGetPatientDrawerPatientQuery: jest.fn(() => ({ currentData: undefined })), -})); - jest.mock('../../../app/components/clinic/PatientForm/SelectTags', () => { const React = require('react'); return jest.fn((props) => ( From 7dce7d207672b1d59f14c0b8785e4083191b1937 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 3 Sep 2026 18:03:18 -0700 Subject: [PATCH 109/123] WEB-4460 address automated review feedback --- app/components/PatientDrawer/useAgpCGM/useAgpCGM.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js index 7500b7c42a..581aa5c779 100644 --- a/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js +++ b/app/components/PatientDrawer/useAgpCGM/useAgpCGM.js @@ -126,7 +126,7 @@ const useAgpCGM = ( dispatch(actions.worker.removeGeneratedPDFS()); dispatch(actions.worker.dataWorkerRemoveDataRequest(null, patientId)); }; - }, []); + }, [patientId]); // Note: probably unnecessary; failsafe to ensure that data is being returned for correct patient const isCorrectPatientInState = pdf.opts?.patient?.id === patientId; From 1e5952129c25b4e2e6856a9af8cfef39e893cd7d Mon Sep 17 00:00:00 2001 From: henry-tp Date: Sat, 5 Sep 2026 18:46:00 -0700 Subject: [PATCH 110/123] WEB-4460 fix import error --- app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index b024d6abf4..be4f48c614 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -1,7 +1,7 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useLocation, useHistory } from 'react-router-dom'; +import { Redirect, useLocation, useHistory } from 'react-router-dom'; import Table from '../../../components/elements/Table'; import { Flex, Text, Box } from 'theme-ui'; @@ -19,7 +19,6 @@ 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'; import PatientDrawerController from './PatientDrawerController'; @@ -32,7 +31,6 @@ const Gap = () => ; const tableContainerProps = { sx: { containerType: 'inline-size' } }; const TideDashboardV2 = ({ api }) => { - const { search } = useLocation(); const { t } = useTranslation(); const { search, pathname } = useLocation(); const history = useHistory(); From 8b2a6b492337126734c3dc40b8ba6c6160da69b7 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 17:01:37 -0700 Subject: [PATCH 111/123] WEB-4460 initialize base branch From 73d83aa62a5018970c44307701f8a3d726b094ee Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 16:55:13 -0700 Subject: [PATCH 112/123] WEB-4460 add modal controllers --- .../DataIssues/DataConnectionsModalController.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataConnectionsModalController.js diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataConnectionsModalController.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataConnectionsModalController.js new file mode 100644 index 0000000000..f8a7673db9 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataConnectionsModalController.js @@ -0,0 +1,12 @@ +import React from 'react'; +import DataConnectionsModal from '../../../../components/datasources/DataConnectionsModal'; + +const DataConnectionsModalController = ({ isOpen, patient, onClose }) => ( + +); + +export default DataConnectionsModalController; From 5b1733a62ecb5b4c4cc9af4068e46ab7796253a4 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Mon, 17 Aug 2026 16:57:32 -0700 Subject: [PATCH 113/123] WEB-4460 add Data Issues segment --- app/components/datasources/DataConnections.js | 26 ++- .../TideDashboardV2/DataIssues/Cells.js | 141 +++++++++++++++ .../TideDashboardV2/DataIssues/DataIssues.js | 160 ++++++++++++++++++ .../TideDashboardV2/DataIssues/index.js | 3 + .../DataIssues/tideDashboardLegacyApi.js | 60 +++++++ .../DataIssues/useTideReportNoDataPatients.js | 35 ++++ .../TideDashboardV2/TideDashboardV2.js | 3 + 7 files changed, 413 insertions(+), 15 deletions(-) create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/index.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js create mode 100644 app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js diff --git a/app/components/datasources/DataConnections.js b/app/components/datasources/DataConnections.js index 33ce193ee6..dccfac989d 100644 --- a/app/components/datasources/DataConnections.js +++ b/app/components/datasources/DataConnections.js @@ -332,26 +332,13 @@ export const getConnectStateUI = (patient, isLoggedInUser, providerName) => { } }; -export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId, setActiveHandler) => reduce(availableProviders, (result, providerName) => { - - const provider = providers[providerName]; +export const resolveConnectState = (patient, providerName, isLoggedInUser = false) => { const dataSource = getCurrentDataSourceForProvider(patient, providerName); const connectStateUI = getConnectStateUI(patient, isLoggedInUser, providerName); const inviteExpired = dataSource?.expirationTime < moment.utc().toISOString(); - // If the provider requires a logged in user to create the connection, then ensure that is the case. - if (!!provider.requiresLoggedInUser && !isLoggedInUser) { - return result; - } - - // If the provider requires an existing data source to create the connection, then ensure that is the case. - // This mechanism can be used to limit access to certain providers to only users who have previously connected - // or where Tidepool has created a data source on their behalf. - if (!!provider.requiresExistingDataSource && !dataSource) { - return result; - } - let connectState; + if (dataSource?.state) { connectState = includes(keys(connectStateUI), dataSource.state) ? dataSource.state @@ -368,6 +355,15 @@ export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId connectState = 'noPendingConnections'; } + return connectState; +}; + +export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId, setActiveHandler) => reduce(availableProviders, (result, providerName) => { + result[providerName] = {}; + + const connectStateUI = getConnectStateUI(patient, isLoggedInUser, providerName); + const connectState = resolveConnectState(patient, providerName, isLoggedInUser); + const { color, icon, message, text, handler } = connectStateUI[connectState]; const { diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js new file mode 100644 index 0000000000..1c7877dad4 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js @@ -0,0 +1,141 @@ +import React, { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import moment from 'moment-timezone'; +import { Box, Text } from 'theme-ui'; +import { resolveConnectState } from '../../../../components/datasources/DataConnections'; +import includes from 'lodash/includes'; + +import Pill from '../../../../components/elements/Pill'; +import HoverButton from '../../../../components/elements/HoverButton'; +import PopoverMenu from '../../../../components/elements/PopoverMenu'; +import { colors, fontWeights } from '../../../../themes/baseTheme'; +import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded'; +import EditIcon from '@material-ui/icons/EditRounded'; +import DataInIcon from '../../../../core/icons/DataInIcon.svg'; + +export const DexcomConnectionStatusCell = ({ patient, onOpenDataConnectionsModal }) => { + const { t } = useTranslation(); + + const dexcomConnectStateUI = useMemo(() => ({ + noPendingConnections: { colorPalette: 'neutral', icon: null, text: t('No Pending Connections') }, + inviteJustSent: { colorPalette: 'info', icon: null, text: t('Invite Sent') }, + pending: { colorPalette: 'info', icon: null, text: t('Invite Sent') }, + pendingReconnect: { colorPalette: 'info', icon: null, text: t('Invite Sent') }, + pendingExpired: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Invite Expired') }, + connected: { colorPalette: 'info', icon: null, text: t('Connected') }, + disconnected: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Patient Disconnected') }, + error: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Error Connecting') }, + unknown: { colorPalette: 'warning', icon: ErrorRoundedIcon, text: t('Unknown Status') }, + }), [t]); + + const dexcomConnectState = resolveConnectState(patient, 'dexcom'); + + if (!dexcomConnectState) return null; + + const showViewButton = includes([ + 'disconnected', + 'error', + 'noPendingConnections', + 'pendingExpired', + 'unknown', + ], dexcomConnectState); + + const handleOpenDataConnectionsModal = () => onOpenDataConnectionsModal(patient.id); + + const StatusBadge = () => ( + + ); + + if (!showViewButton) return ; + + return ( + + + + + + ); +}; + +export const DaysSinceLastDataCell = ({ patient }) => { + const timePrefs = useSelector(state => state.blip.timePrefs); + + const daysSinceLastData = useMemo(() => { + if (!patient?.lastData) return null; + + const timezone = timePrefs?.timezoneName || new Intl.DateTimeFormat().resolvedOptions().timeZone; + const startOfLastDataDay = moment.utc(patient.lastData).tz(timezone).startOf('day'); + const startOfCurrentDay = moment.utc().tz(timezone).startOf('day'); + + return startOfCurrentDay.diff(startOfLastDataDay, 'days'); + }, [patient?.lastData, timePrefs?.timezoneName]); + + return ( + {daysSinceLastData ?? '-'} + ); +}; + +export const MoreMenuCell = ({ patient, onOpenEditPatientDialog, onOpenDataConnectionsModal }) => { + const { t } = useTranslation(); + + return ( + { + _popupState.close(); + onOpenEditPatientDialog(patient.id); + }, + text: t('Edit Patient Details'), + }, { + iconSrc: DataInIcon, + iconLabel: t('Bring Data into Tidepool'), + iconPosition: 'left', + id: `edit-data-connections-${patient?.id}`, + variant: 'actionListItem', + onClick: (_popupState) => { + _popupState.close(); + onOpenDataConnectionsModal(patient.id); + }, + text: t('Bring Data into Tidepool'), + }]} + sx={{ position: 'relative', left: '-2px' }} + /> + ); +}; + +export default { + DexcomConnectionStatusCell, + DaysSinceLastDataCell, + MoreMenuCell, +}; diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js new file mode 100644 index 0000000000..2185cb6f9f --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js @@ -0,0 +1,160 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Box, Flex, Text } from 'theme-ui'; +import { colors as vizColors } from '@tidepool/viz'; +import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; + +import Table from '../../../../components/elements/Table'; + +import { PatientCell } from '../Cells'; +import { DexcomConnectionStatusCell, DaysSinceLastDataCell, MoreMenuCell } from './Cells'; +import TagListCell from '../../components/TagListCell'; +import EmptyContentNode from '../EmptyContentNode'; +import useTideReportNoDataPatients from './useTideReportNoDataPatients'; +import EditPatientDialogController from './EditPatientDialogController'; +import DataConnectionsModalController from './DataConnectionsModalController'; +import { useGetPatientFromClinicQuery } from './tideDashboardLegacyApi'; +import Icon from '../../../../components/elements/Icon'; + +const usePatientFromClinic = (patientId) => { + const selectedClinicId = useSelector(state => state.blip.selectedClinicId); + + const { data: patient } = useGetPatientFromClinicQuery( + { clinicId: selectedClinicId, patientId }, + { skip: !selectedClinicId || !patientId } + ); + + return patient; +}; + +const DataIssues = ({ api }) => { + const { t } = useTranslation(); + const { patients } = useTideReportNoDataPatients(); + + const [activePatientId, setActivePatientId] = useState(null); + const [isAccordionOpen, setIsAccordionOpen] = useState(false); + const [isEditPatientDialogOpen, setIsEditPatientDialogOpen] = useState(false); + const [isDataConnectionsModalOpen, setIsDataConnectionsModalOpen] = useState(false); + + const activePatient = usePatientFromClinic(activePatientId); + + const handleOpenEditPatientDialog = useCallback((patientId) => { + setActivePatientId(patientId); + setIsEditPatientDialogOpen(true); + }, []); + + const handleOpenDataConnectionsModal = (patientId) => { + setActivePatientId(patientId); + setIsDataConnectionsModalOpen(true); + }; + + const handleCloseModals = () => { + setIsEditPatientDialogOpen(false); + setIsDataConnectionsModalOpen(false); + setActivePatientId(null); + }; + + const columns = useMemo(() => ([ + { + title: t('Patient Details'), + field: 'fullName', + align: 'left', + render: patient => , + }, + { + title: t('Dexcom Connection Status'), + field: 'dexcomConnectionStatus', + align: 'left', + render: patient => , + }, + { + title: t('Days Since Last Data'), + field: 'daysSinceLastData', + align: 'center', + render: patient => , + }, + { + title: t('Tags'), + field: 'tags', + align: 'center', + render: patient => , + }, + { + title: '', + field: 'moreMenu', + align: 'center', + render: patient => ( + + ), + }, + ]), [t, handleOpenEditPatientDialog, handleOpenDataConnectionsModal]); + + if (!patients?.length) return null; + + return ( + + setIsAccordionOpen(isOpen => !isOpen)} + className="data-issues-section-label" + sx={{ + justifyContent: 'space-between', + transition: 'all 0.2s ease', + borderBottom: '1px solid', + borderColor: isAccordionOpen ? vizColors.white : vizColors.gray10, + padding: 3, + color: vizColors.blueGray50, + '&:hover': { cursor: 'pointer', borderColor: vizColors.gray30 }, + }} + > + {t('Device Issues ({{count}})', { count: patients.length })} + + + + + + +
} + containerProps={{ sx: { containerType: 'inline-size' } }} + /> + + + + + + + + ); +}; + +export default DataIssues; diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/index.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/index.js new file mode 100644 index 0000000000..cacca4eddb --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/index.js @@ -0,0 +1,3 @@ +import DataIssues from './DataIssues'; + +export default DataIssues; diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js new file mode 100644 index 0000000000..8040d16209 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js @@ -0,0 +1,60 @@ +import { RTKQueryApi } from '../../../../redux/api/baseApi'; +import { tagTypes } from '../tideDashboardApi'; + +// This file isolates legacy TIDE Dashboard API calls (the V1 +// `/v1/clinics/:clinicId/tide_report` endpoint) so they are not entangled with +// the new V2 endpoints in `tideDashboardApi`. It shares the primary +// `RTKQueryApi` instance (so no additional store wiring is required), but should +// be kept separate from new work. Prefer `tideDashboardApi` for all new code. + +const { TIDE_DASHBOARD_PATIENTS } = tagTypes; + +export const buildGetTideReportParams = (period, lastData, tags = [], lastDataCutoff, categories = []) => { + const formattedTags = tags.length > 0 ? tags.join(',') : undefined; + const formattedCategories = categories.length > 0 ? categories.join(',') : undefined; + + return { + period, + lastData, + tags: formattedTags, + lastDataCutoff, + categories: formattedCategories, + }; +}; + +const tideDashboardLegacyApi = RTKQueryApi.injectEndpoints({ + endpoints: (builder) => ({ + getTideReport: builder.query({ + query: ({ clinicId, period, lastData, tags, lastDataCutoff, categories }) => { + const params = buildGetTideReportParams(period, lastData, tags, lastDataCutoff, categories); + + return { + url: `/clinics/${clinicId}/tide_report`, + params, + }; + }, + // The tide_report response groups patients by category. The "Data Issues" + // table only renders the `noData` group. We flatten each entry so that the + // row === patient (matching how the V2 table cells consume rows), keeping + // the top-level `lastData` needed to derive days since last data. + transformResponse: (response) => { + const noData = response?.results?.noData || []; + + return { + patients: noData.map(({ patient, lastData }) => ({ ...patient, lastData })), + }; + }, + providesTags: [TIDE_DASHBOARD_PATIENTS], + }), + getPatientFromClinic: builder.query({ + queryFn: async ({ clinicId, patientId }, _queryApi, _extraOptions, baseQuery) => { + if (!patientId) return { data: null }; + + return baseQuery({ url: `/clinics/${clinicId}/patients/${patientId}` }); + }, + providesTags: [TIDE_DASHBOARD_PATIENTS], + }), + }), +}); + +export const { useGetTideReportQuery, useGetPatientFromClinicQuery } = tideDashboardLegacyApi; diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js new file mode 100644 index 0000000000..d2d1d38d87 --- /dev/null +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js @@ -0,0 +1,35 @@ +import { useSelector } from 'react-redux'; +import { useGetTideReportQuery } from './tideDashboardLegacyApi'; +import useDerivedDataRecencyEndpoints from '../useDerivedDataRecencyEndpoints'; + +// The Data Issues table always requests the report; `categories` is a required +// param on the endpoint, but the `noData` group is returned regardless of which +// category is requested, so we send a single category to satisfy the contract. +const CATEGORIES = ['meetingTargets']; + +const useTideReportNoDataPatients = () => { + const selectedClinicId = useSelector(state => state.blip.selectedClinicId); + const { summaryPeriod, lastData, patientTags } = useSelector(state => state.blip.tideDashboardFilters); + + // `lastDataFrom` is derived identically to V1's `lastDataCutoff`. + const [lastDataCutoff] = useDerivedDataRecencyEndpoints(); + + const result = useGetTideReportQuery( + { + clinicId: selectedClinicId, + period: summaryPeriod, + lastData, + tags: patientTags, + lastDataCutoff, + categories: CATEGORIES, + }, + { skip: !selectedClinicId } + ); + + return { + ...result, + patients: result.data?.patients || [], + }; +}; + +export default useTideReportNoDataPatients; diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index be4f48c614..4ab71b17f5 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -25,6 +25,7 @@ import PatientDrawerController from './PatientDrawerController'; import EditPatientDialogController from './modals/EditPatientDialogController'; import DataConnectionsModalController from './modals/DataConnectionsModalController'; import { OVERVIEW_TAB_INDEX } from '../../../components/PatientDrawer/MenuBar'; +import DataIssues from './DataIssues/DataIssues'; const Gap = () => ; @@ -99,6 +100,8 @@ const TideDashboardV2 = ({ api }) => { + + From c6e2d8a228350a679cfa6e5353041c6bb736bc32 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 20 Aug 2026 09:29:22 -0700 Subject: [PATCH 114/123] WEB-4460 fix refactor dropped lines --- app/components/datasources/DataConnections.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/components/datasources/DataConnections.js b/app/components/datasources/DataConnections.js index dccfac989d..3376c04374 100644 --- a/app/components/datasources/DataConnections.js +++ b/app/components/datasources/DataConnections.js @@ -359,7 +359,21 @@ export const resolveConnectState = (patient, providerName, isLoggedInUser = fals }; export const getDataConnectionProps = (patient, isLoggedInUser, selectedClinicId, setActiveHandler) => reduce(availableProviders, (result, providerName) => { - result[providerName] = {}; + + const provider = providers[providerName]; + const dataSource = getCurrentDataSourceForProvider(patient, providerName); + + // If the provider requires a logged in user to create the connection, then ensure that is the case. + if (!!provider.requiresLoggedInUser && !isLoggedInUser) { + return result; + } + + // If the provider requires an existing data source to create the connection, then ensure that is the case. + // This mechanism can be used to limit access to certain providers to only users who have previously connected + // or where Tidepool has created a data source on their behalf. + if (!!provider.requiresExistingDataSource && !dataSource) { + return result; + } const connectStateUI = getConnectStateUI(patient, isLoggedInUser, providerName); const connectState = resolveConnectState(patient, providerName, isLoggedInUser); From 1c2f77f2b89853842dd50f5e8fa14f4a503e4a48 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 18:30:35 -0700 Subject: [PATCH 115/123] WEB-4460 add PatientLastReviewed to Data Issues --- .../TideDashboardV2/DataIssues/Cells.js | 7 +++++++ .../TideDashboardV2/DataIssues/DataIssues.js | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js index 1c7877dad4..10cc87fa57 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js @@ -13,6 +13,7 @@ import { colors, fontWeights } from '../../../../themes/baseTheme'; import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded'; import EditIcon from '@material-ui/icons/EditRounded'; import DataInIcon from '../../../../core/icons/DataInIcon.svg'; +import PatientLastReviewed from '../../components/ReviewPatientToggle/PatientLastReviewed'; export const DexcomConnectionStatusCell = ({ patient, onOpenDataConnectionsModal }) => { const { t } = useTranslation(); @@ -100,6 +101,12 @@ export const DaysSinceLastDataCell = ({ patient }) => { ); }; +export const PatientLastReviewedCell = ({ patient }) => { + return + + ; +}; + export const MoreMenuCell = ({ patient, onOpenEditPatientDialog, onOpenDataConnectionsModal }) => { const { t } = useTranslation(); diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js index 2185cb6f9f..61318fb731 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js @@ -9,8 +9,15 @@ import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; import Table from '../../../../components/elements/Table'; import { PatientCell } from '../Cells'; -import { DexcomConnectionStatusCell, DaysSinceLastDataCell, MoreMenuCell } from './Cells'; import TagListCell from '../../components/TagListCell'; + +import { + DexcomConnectionStatusCell, + DaysSinceLastDataCell, + PatientLastReviewedCell, + MoreMenuCell, +} from './Cells'; + import EmptyContentNode from '../EmptyContentNode'; import useTideReportNoDataPatients from './useTideReportNoDataPatients'; import EditPatientDialogController from './EditPatientDialogController'; @@ -81,6 +88,12 @@ const DataIssues = ({ api }) => { align: 'center', render: patient => , }, + { + title: t('Last Reviewed'), + field: 'lastReviewed', + align: 'center', + render: patient => , + }, { title: '', field: 'moreMenu', From 5d0c2bc0e36d06086b6ebbf5901b01beebe00058 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 27 Aug 2026 21:19:58 -0700 Subject: [PATCH 116/123] WEB-4460 add tests for Data Issues --- .../TideDashboardV2/DataIssues/Cells.test.js | 212 ++++++++++++++++++ .../DataIssues/DataIssues.test.js | 127 +++++++++++ .../TideDashboardV2/TideDashboardV2.test.js | 17 +- .../TideDashboardV2/DataIssues/Cells.js | 1 + .../TideDashboardV2/DataIssues/DataIssues.js | 2 +- .../TideDashboardV2/TideDashboardV2.js | 1 + 6 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 __tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.test.js create mode 100644 __tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.test.js diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.test.js new file mode 100644 index 0000000000..34c1b79a62 --- /dev/null +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.test.js @@ -0,0 +1,212 @@ +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 { + DexcomConnectionStatusCell, + MoreMenuCell, +} from '@app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells'; + +const mockStore = configureStore([thunk]); + +describe('DataIssues Cells', () => { + let store; + + const renderComponent = (cell) => { + render({cell}); + }; + + beforeEach(() => { + store = mockStore({ + blip: { + selectedClinicId: 'clinic123', + timePrefs: {}, + }, + }); + + // Fake only Date so "now" is pinned; connect states and day counts resolve deterministically + jest.useFakeTimers({ + now: new Date('2025-05-29T10:00:00Z'), + doNotFake: [ + 'hrtime', 'nextTick', 'performance', 'queueMicrotask', + 'requestAnimationFrame', 'cancelAnimationFrame', + 'requestIdleCallback', 'cancelIdleCallback', + 'setImmediate', 'clearImmediate', + 'setInterval', 'clearInterval', + 'setTimeout', 'clearTimeout', + ], + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('DexcomConnectionStatusCell', () => { + const onOpenDataConnectionsModal = jest.fn(); + + const ui = (patient) => ( + + + + ); + + beforeEach(() => { + onOpenDataConnectionsModal.mockClear(); + }); + + it('shows the connection status for each dexcom data source state, with a View button that opens the Data Connections modal for actionable states', async () => { + // No dexcom data source: No Pending Connections, and View opens the Data Connections modal + const { rerender } = render(ui({ id: 'patient-1' })); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('No Pending Connections'); + await userEvent.click(screen.getByRole('button', { name: 'View' })); + expect(onOpenDataConnectionsModal).toHaveBeenCalledWith('patient-1'); + + // Pending invite: Invite Sent, without a View button + rerender(ui( + { + id: 'patient-1', + dataSources: [{ + providerName: 'dexcom', + state: 'pending', + modifiedTime: '2025-05-27T10:00:00.000Z', + expirationTime: '2025-06-03T10:00:00.000Z', + }], + } + )); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('Invite Sent'); + expect(screen.queryByRole('button', { name: 'View' })).not.toBeInTheDocument(); + + // Expired pending invite: Invite Expired, with a View button + rerender(ui( + { + id: 'patient-1', + dataSources: [{ + providerName: 'dexcom', + state: 'pending', + modifiedTime: '2025-04-01T10:00:00.000Z', + expirationTime: '2025-05-01T10:00:00.000Z', + }], + } + )); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('Invite Expired'); + expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument(); + + // Active connection: Connected, without a View button + rerender(ui( + { + id: 'patient-1', + dataSources: [{ + providerName: 'dexcom', + state: 'connected', + modifiedTime: '2025-05-27T10:00:00.000Z', + lastImportTime: '2025-05-28T10:00:00.000Z', + latestDataTime: '2025-05-28T10:00:00.000Z', + }], + } + )); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('Connected'); + expect(screen.queryByRole('button', { name: 'View' })).not.toBeInTheDocument(); + + // Patient has disconnected: Patient Disconnected, with a View button + rerender(ui( + { + id: 'patient-1', + dataSources: [{ + providerName: 'dexcom', + state: 'disconnected', + modifiedTime: '2025-05-20T10:00:00.000Z', + }], + } + )); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('Patient Disconnected'); + expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument(); + + // Connection error: Error Connecting, with a View button + rerender(ui( + { + id: 'patient-1', + dataSources: [{ + providerName: 'dexcom', + state: 'error', + modifiedTime: '2025-05-20T10:00:00.000Z', + }], + } + )); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('Error Connecting'); + expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument(); + + // Unrecognized data source state: Unknown Status, with a View button + rerender(ui( + { + id: 'patient-1', + dataSources: [{ + providerName: 'dexcom', + state: 'somethingUnexpected', + modifiedTime: '2025-05-20T10:00:00.000Z', + }], + } + )); + + expect(screen.getByLabelText('dexcom connection status')).toHaveTextContent('Unknown Status'); + expect(screen.getByRole('button', { name: 'View' })).toBeInTheDocument(); + }); + }); + + describe('MoreMenuCell', () => { + const patient = { id: 'patient-1', fullName: 'James Jellyfish' }; + const onOpenEditPatientDialog = jest.fn(); + const onOpenDataConnectionsModal = jest.fn(); + + beforeEach(() => { + onOpenEditPatientDialog.mockClear(); + onOpenDataConnectionsModal.mockClear(); + }); + + it('opens the Edit Patient dialog for the patient', async () => { + renderComponent( + + ); + + expect(screen.queryByRole('button', { name: /Edit Patient Details/ })).not.toBeInTheDocument(); + await userEvent.click(screen.getByTestId('action-menu-patient-1-icon')); + + await userEvent.click(screen.getByRole('button', { name: /Edit Patient Details/ })); + expect(onOpenEditPatientDialog).toHaveBeenCalledWith('patient-1'); + expect(onOpenDataConnectionsModal).not.toHaveBeenCalled(); + }); + + it('opens the Data Connections modal for the patient', async () => { + renderComponent( + + ); + + expect(screen.queryByRole('button', { name: /Bring Data into Tidepool/ })).not.toBeInTheDocument(); + await userEvent.click(screen.getByTestId('action-menu-patient-1-icon')); + + await userEvent.click(screen.getByRole('button', { name: /Bring Data into Tidepool/ })); + expect(onOpenDataConnectionsModal).toHaveBeenCalledWith('patient-1'); + expect(onOpenEditPatientDialog).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.test.js new file mode 100644 index 0000000000..1997b84e73 --- /dev/null +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.test.js @@ -0,0 +1,127 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +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'; +import moment from 'moment'; + +import { setupStore } from '@tests/utils/setupStore'; +import blipReducer from '@app/redux/reducers'; +import DataIssues from '@app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues'; + +// Pin the data recency window to a stable [lastDataFrom, lastDataTo] +jest.mock('@app/pages/clinicworkspace/TideDashboardV2/useDerivedDataRecencyEndpoints', () => ({ + __esModule: true, + default: () => [ + '2025-05-23T00:00:00.000Z', // lastDataFrom + '2025-05-30T00:00:00.000Z', // lastDataTo + ], +})); + +jest.mock('@app/core/api', () => ({ clinics: { getPatientFromClinic: jest.fn() } })); + +jest.mock('@app/providers/ToastProvider', () => ({ + useToasts: jest.fn().mockReturnValue({ set: jest.fn() }), +})); + +const tideReportResponse = { + results: { + noData: [{ + patient: { + id: 'patient-1', + fullName: 'No Data Patient 1', + birthDate: '2001-01-01', + mrn: 'mrn-001', + tags: ['tag8'], + dataSources: [{ providerName: 'dexcom', state: 'error', modifiedTime: '2025-05-01T00:00:00.000Z' }], + }, + lastData: moment.utc().subtract(3, 'days').toISOString(), + }, + { + patient: { + id: 'patient-2', + fullName: 'No Data Patient 2', + birthDate: '2002-02-02', + mrn: 'mrn-002', + }, + lastData: moment.utc().subtract(12, 'days').toISOString(), + }], + }, +}; + +const server = setupServer( + http.get('http://app.tidepool.test/v1/clinics/clinic123/tide_report', ({ request }) => { + const searchParams = Object.fromEntries(new URL(request.url).searchParams); + + const anticipatedQuery = { + period: '30d', + lastData: '14', + tags: 'tag8', + lastDataCutoff: '2025-05-23T00:00:00.000Z', + categories: 'meetingTargets', + }; + + if (!isEqual(searchParams, anticipatedQuery)) throw new Error('Unexpected tide report query'); + + return HttpResponse.json(tideReportResponse); + }), +); + +describe('DataIssues', () => { + let store; + + const api = { clinics: { getPatientFromClinic: jest.fn() } }; + + const renderComponent = () => render( + + + + + + ); + + beforeAll(() => server.listen()); + + beforeEach(() => { + store = setupStore({ + blip: { + selectedClinicId: 'clinic123', + clinics: { clinic123: { id: 'clinic123', patientTags: [{ id: 'tag8', name: 'Tag 8' }] } }, + tideDashboardFilters: { lastData: 14, patientTags: ['tag8'], clinicSites: [], summaryPeriod: '30d' }, + }, + }, { blip: blipReducer }); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + afterAll(() => server.close()); + + it('fetches the tide report with the applied filters and renders the patients from the noData group', async () => { + renderComponent(); + + // The section header shows the count of patients with data issues + expect(await screen.findByText('Device Issues (2)')).toBeInTheDocument(); + + expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Dexcom Connection Status/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Days Since Last Data/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + + expect(screen.getByText('No Data Patient 1')).toBeInTheDocument(); + expect(screen.getByText('DOB: 2001-01-01')).toBeInTheDocument(); + expect(screen.getByText('MRN: mrn-001')).toBeInTheDocument(); + expect(screen.getByText('No Data Patient 2')).toBeInTheDocument(); + + // The Dexcom connection status resolves from each patient's data sources + expect(screen.getByText('Error Connecting')).toBeInTheDocument(); + expect(screen.getByText('No Pending Connections')).toBeInTheDocument(); + + // Tags resolve against the clinic's patient tags + expect(screen.getByText('Tag 8')).toBeInTheDocument(); + }); +}); diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js index e8a6be7c5e..7e64e15dc9 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js @@ -197,7 +197,11 @@ const server = setupServer( const patients = getCorrespondingDataForQuery(searchParams); return HttpResponse.json({ data: patients, meta: { count: patients.length } }); - }) + }), + + http.get('http://app.tidepool.test/v1/clinics/clinic123/tide_report', () => HttpResponse.json({ + results: { noData: [{ patient: { id: 'no-data-1', fullName: 'No Data Patient 1', birthDate: '2006-01-01' } }] }, + })) ); describe('TideDashboardV2', () => { @@ -226,6 +230,8 @@ describe('TideDashboardV2', () => { it('fetches and renders each category of patients', async () => { renderComponent(); + const table = await screen.findByTestId('tideDashboardPatientsTable'); + // All Patients is the pre-selected category expect(await screen.findByText('Default Patient 1')).toBeInTheDocument(); @@ -233,7 +239,7 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Default Patient 2')).toBeInTheDocument(); expect(screen.getByText('DOB: 2001-01-01')).toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Flag/ })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); @@ -392,6 +398,13 @@ describe('TideDashboardV2', () => { expect(await screen.findByText('Filtered Patient 3')).toBeInTheDocument(); }, TEST_TIMEOUT_MS); + it('renders the Data Issues section when the tide report returns patients with no data', async () => { + renderComponent(); + + expect(await screen.findByText('Device Issues (1)')).toBeInTheDocument(); + expect(await screen.findByText('No Data Patient 1')).toBeInTheDocument(); + }, TEST_TIMEOUT_MS); + describe('empty content', () => { beforeEach(() => { let fetchCount = 0; diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js index 10cc87fa57..ab5c1262a0 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js @@ -113,6 +113,7 @@ export const MoreMenuCell = ({ patient, onOpenEditPatientDialog, onOpenDataConne return ( { const columns = useMemo(() => ([ { - title: t('Patient Details'), + title: t('Patient Name'), field: 'fullName', align: 'left', render: patient => , diff --git a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js index 4ab71b17f5..7a1f8a6ad7 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js +++ b/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.js @@ -89,6 +89,7 @@ const TideDashboardV2 = ({ api }) => {
Date: Fri, 28 Aug 2026 00:06:10 -0700 Subject: [PATCH 117/123] WEB-4460 fix tests --- .../TideDashboardV2/TideDashboardV2.test.js | 20 +++++++++---------- .../TideDashboardV2/DataIssues/DataIssues.js | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js index 7e64e15dc9..75733658e5 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js @@ -240,16 +240,16 @@ describe('TideDashboardV2', () => { expect(screen.getByText('DOB: 2001-01-01')).toBeInTheDocument(); expect(within(table).getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Flag/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Flag/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Very Low fetches and shows the Very Low cohort await userEvent.click(screen.getByRole('radio', { name: /Very Low/ })); diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js index 2f52a2fdbb..61318fb731 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/DataIssues.js @@ -65,7 +65,7 @@ const DataIssues = ({ api }) => { const columns = useMemo(() => ([ { - title: t('Patient Name'), + title: t('Patient Details'), field: 'fullName', align: 'left', render: patient => , From f04ef5a89eaf9cf1781e28e255b65f1c2e443720 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Fri, 28 Aug 2026 17:56:59 -0700 Subject: [PATCH 118/123] WEB-4460 fix DataConnections cell size --- .../TideDashboardV2/DataIssues/Cells.js | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js index ab5c1262a0..5de8ebafc7 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js @@ -58,28 +58,30 @@ export const DexcomConnectionStatusCell = ({ patient, onOpenDataConnectionsModal if (!showViewButton) return ; return ( - + - - - - + }} + > + + + + + ); }; From 68134d98e496b0a26eb5cbcec09c745b3e7c47af Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 15:48:43 -0700 Subject: [PATCH 119/123] WEB-4460 use generic adapter for data issues cells --- app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js index 5de8ebafc7..b3fc281e87 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/Cells.js @@ -13,7 +13,7 @@ import { colors, fontWeights } from '../../../../themes/baseTheme'; import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded'; import EditIcon from '@material-ui/icons/EditRounded'; import DataInIcon from '../../../../core/icons/DataInIcon.svg'; -import PatientLastReviewed from '../../components/ReviewPatientToggle/PatientLastReviewed'; +import PatientLastReviewedGenericAdapter from '../../components/ReviewPatientToggle/PatientLastReviewedGenericAdapter'; export const DexcomConnectionStatusCell = ({ patient, onOpenDataConnectionsModal }) => { const { t } = useTranslation(); @@ -105,7 +105,7 @@ export const DaysSinceLastDataCell = ({ patient }) => { export const PatientLastReviewedCell = ({ patient }) => { return - + ; }; From a91deb5ec9c08381d0c9bc3db983dcc311fc46b4 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 15:53:44 -0700 Subject: [PATCH 120/123] WEB-4460 fix table test to search within patients table --- .../TideDashboardV2/TideDashboardV2.test.js | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js index 75733658e5..3d6fad214d 100644 --- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js +++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/TideDashboardV2.test.js @@ -258,17 +258,17 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Default Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time < 70/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time < 70/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Low fetches and shows the Low cohort await userEvent.click(screen.getByRole('radio', { name: /^Low$/ })); @@ -277,17 +277,17 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very Low Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time < 70/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time < 54/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time < 70/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Drop in TIR fetches and shows the Drop in TIR cohort await userEvent.click(screen.getByRole('radio', { name: /Drop in TIR/ })); @@ -296,17 +296,17 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Drop In TIR Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting High fetches and shows the High cohort await userEvent.click(screen.getByRole('radio', { name: /^High$/ })); @@ -315,17 +315,17 @@ describe('TideDashboardV2', () => { expect(screen.getByText('High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Drop In TIR Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time > 180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time > 180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Very High fetches and shows the Very High cohort await userEvent.click(screen.getByRole('radio', { name: /Very High/ })); @@ -334,17 +334,17 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Very High Patient 2')).toBeInTheDocument(); expect(screen.queryByText('High Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Time > 180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time > 250/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Time > 180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Low CGM Wear fetches and shows the Low CGM Wear cohort await userEvent.click(screen.getByRole('radio', { name: /Low CGM Wear/ })); @@ -353,17 +353,17 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Low CGM Wear Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Very High Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(10); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(10); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% TIR 70-180/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); // Selecting Meeting Targets fetches and shows the Meeting Targets cohort await userEvent.click(screen.getByRole('radio', { name: /Meeting Targets/ })); @@ -372,16 +372,16 @@ describe('TideDashboardV2', () => { expect(screen.getByText('Meeting Targets Patient 2')).toBeInTheDocument(); expect(screen.queryByText('Low CGM Wear Patient 1')).not.toBeInTheDocument(); - expect(screen.getAllByRole('columnheader')).toHaveLength(9); - expect(screen.getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); - expect(screen.getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); + expect(within(table).getAllByRole('columnheader')).toHaveLength(9); + expect(within(table).getByRole('columnheader', { name: /Patient Details/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Avg Glucose/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Time in Range/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /% Change in TIR/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /GMI/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /CGM Use/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Tags/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /Last Reviewed/ })).toBeInTheDocument(); + expect(within(table).getByRole('columnheader', { name: /More Options/ })).toBeInTheDocument(); }, TEST_TIMEOUT_MS); it('fetches with filters', async () => { From cd75cfd09d6e14d40795c9c4ab43ed1b339bac40 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 1 Sep 2026 16:13:04 -0700 Subject: [PATCH 121/123] WEB-4460 fix sites not being passed --- .../DataIssues/tideDashboardLegacyApi.js | 14 +++++++------- .../DataIssues/useTideReportNoDataPatients.js | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js index 8040d16209..0f737955bc 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js @@ -9,7 +9,7 @@ import { tagTypes } from '../tideDashboardApi'; const { TIDE_DASHBOARD_PATIENTS } = tagTypes; -export const buildGetTideReportParams = (period, lastData, tags = [], lastDataCutoff, categories = []) => { +export const buildGetTideReportParams = (period, lastData, lastDataCutoff, tags = [], sites = [], categories = []) => { const formattedTags = tags.length > 0 ? tags.join(',') : undefined; const formattedCategories = categories.length > 0 ? categories.join(',') : undefined; @@ -25,21 +25,21 @@ export const buildGetTideReportParams = (period, lastData, tags = [], lastDataCu const tideDashboardLegacyApi = RTKQueryApi.injectEndpoints({ endpoints: (builder) => ({ getTideReport: builder.query({ - query: ({ clinicId, period, lastData, tags, lastDataCutoff, categories }) => { - const params = buildGetTideReportParams(period, lastData, tags, lastDataCutoff, categories); + query: ({ clinicId, period, lastData, tags, sites, lastDataCutoff, categories }) => { + const params = buildGetTideReportParams(period, lastData, lastDataCutoff, tags, sites, categories); return { url: `/clinics/${clinicId}/tide_report`, params, }; }, - // The tide_report response groups patients by category. The "Data Issues" - // table only renders the `noData` group. We flatten each entry so that the - // row === patient (matching how the V2 table cells consume rows), keeping - // the top-level `lastData` needed to derive days since last data. transformResponse: (response) => { const noData = response?.results?.noData || []; + // The tide_report response groups patients by category. The "Data Issues" + // table only renders the `noData` group. We flatten each entry so that the + // row === patient (matching how the V2 table cells consume rows), keeping + // the top-level `lastData` needed to derive days since last data. return { patients: noData.map(({ patient, lastData }) => ({ ...patient, lastData })), }; diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js index d2d1d38d87..39864df9a7 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/useTideReportNoDataPatients.js @@ -9,7 +9,7 @@ const CATEGORIES = ['meetingTargets']; const useTideReportNoDataPatients = () => { const selectedClinicId = useSelector(state => state.blip.selectedClinicId); - const { summaryPeriod, lastData, patientTags } = useSelector(state => state.blip.tideDashboardFilters); + const { summaryPeriod, lastData, patientTags, clinicSites } = useSelector(state => state.blip.tideDashboardFilters); // `lastDataFrom` is derived identically to V1's `lastDataCutoff`. const [lastDataCutoff] = useDerivedDataRecencyEndpoints(); @@ -20,6 +20,7 @@ const useTideReportNoDataPatients = () => { period: summaryPeriod, lastData, tags: patientTags, + sites: clinicSites, lastDataCutoff, categories: CATEGORIES, }, From 95e1b00edf06ee3b456a07df183b25a647df1bb5 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Thu, 3 Sep 2026 18:08:25 -0700 Subject: [PATCH 122/123] WEB-4460 address automated review feedback --- .../TideDashboardV2/DataIssues/tideDashboardLegacyApi.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js index 0f737955bc..af1511a10f 100644 --- a/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js +++ b/app/pages/clinicworkspace/TideDashboardV2/DataIssues/tideDashboardLegacyApi.js @@ -11,12 +11,14 @@ const { TIDE_DASHBOARD_PATIENTS } = tagTypes; export const buildGetTideReportParams = (period, lastData, lastDataCutoff, tags = [], sites = [], categories = []) => { const formattedTags = tags.length > 0 ? tags.join(',') : undefined; + const formattedSites = sites.length > 0 ? sites.join(',') : undefined; const formattedCategories = categories.length > 0 ? categories.join(',') : undefined; return { period, lastData, tags: formattedTags, + sites: formattedSites, lastDataCutoff, categories: formattedCategories, }; From 34c1b3544ec3bad9aaf13d5d95de0c140c2f0ef5 Mon Sep 17 00:00:00 2001 From: henry-tp Date: Tue, 8 Sep 2026 14:22:37 -0700 Subject: [PATCH 123/123] WEB-4454 initial commit