diff --git a/__tests__/unit/app/components/navpatientheader/index.test.js b/__tests__/unit/app/components/navpatientheader/index.test.js
index e07501c918..1ec023440c 100644
--- a/__tests__/unit/app/components/navpatientheader/index.test.js
+++ b/__tests__/unit/app/components/navpatientheader/index.test.js
@@ -7,7 +7,7 @@
import React from 'react';
import { Provider } from 'react-redux';
-import { render, screen } from '@testing-library/react';
+import { render, screen, within } from '@testing-library/react';
import _ from 'lodash';
import configureStore from 'redux-mock-store';
import { thunk } from 'redux-thunk';
@@ -39,7 +39,7 @@ jest.mock('@app/providers/ToastProvider', () => ({
describe('NavPatientHeader', () => {
const trackMetric = jest.fn();
- const api = {};
+ const api = { clinics: { getPatientFromClinic: jest.fn() } };
const patientProps = {
userid: '1234',
@@ -191,7 +191,7 @@ describe('NavPatientHeader', () => {
expect(screen.getByText(/Naoya Inoue/)).toBeInTheDocument();
expect(screen.getByText(/999999/)).toBeInTheDocument();
expect(screen.getByText(/1965-01-01/)).toBeInTheDocument();
- expect(screen.getByText(/Pre-diabetes/)).toBeInTheDocument();
+ expect(within(screen.getByTestId('nav-patient-header')).getByText(/Pre-diabetes/)).toBeInTheDocument();
// should NOT show demographic info from the 'patient' object
expect(screen.queryByText(/Vasyl Lomachenko/)).not.toBeInTheDocument();
@@ -228,7 +228,7 @@ describe('NavPatientHeader', () => {
expect(screen.getByText(/Naoya Inoue/)).toBeInTheDocument();
expect(screen.getByText(/999999/)).toBeInTheDocument();
expect(screen.getByText(/1965-01-01/)).toBeInTheDocument();
- expect(screen.getByText(/Pre-diabetes/)).toBeInTheDocument();
+ expect(within(screen.getByTestId('nav-patient-header')).getByText(/Pre-diabetes/)).toBeInTheDocument();
// should NOT show demographic info from the 'patient' object
expect(screen.queryByText(/Vasyl Lomachenko/)).not.toBeInTheDocument();
diff --git a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js
index 4dc5f527b4..cc73f19249 100644
--- a/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js
+++ b/__tests__/unit/app/pages/clinicworkspace/TideDashboardV2/Cells.test.js
@@ -267,4 +267,34 @@ describe('Cells', () => {
expect(screen.queryByText('Very Low')).not.toBeInTheDocument();
});
});
+
+ describe('MoreMenuCell', () => {
+ it('dispatches the actions to open 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(store.getActions()).toStrictEqual([
+ { type: 'tideDashboard/setEditPatientDialogIsOpen', payload: true },
+ { type: 'tideDashboard/setEditPatientDialogPatientId', payload: 'patient-1' },
+ ]);
+ });
+
+ it('dispatches the actions to open 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(store.getActions()).toStrictEqual([
+ { type: 'tideDashboard/setDataConnectionsModalIsOpen', payload: true },
+ { type: 'tideDashboard/setDataConnectionsModalPatientId', payload: 'patient-1' },
+ ]);
+ });
+ });
});
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/EditPatientDialog.test.js b/__tests__/unit/components/navpatientheader/EditPatientDialogController.test.js
similarity index 56%
rename from __tests__/unit/components/navpatientheader/EditPatientDialog.test.js
rename to __tests__/unit/components/navpatientheader/EditPatientDialogController.test.js
index 98a8b868e6..c20888b652 100644
--- a/__tests__/unit/components/navpatientheader/EditPatientDialog.test.js
+++ b/__tests__/unit/components/navpatientheader/EditPatientDialogController.test.js
@@ -2,13 +2,14 @@ import React from 'react';
import { createStore, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk';
import { Provider } from 'react-redux';
+import { MemoryRouter } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import configureStore from 'redux-mock-store';
import { ThemeProvider } from 'theme-ui';
import { utils as vizUtils } from '@tidepool/viz';
import theme from '@app/themes/baseTheme';
-import EditPatientDialog from '@app/components/navpatientheader/EditPatientDialog';
+import EditPatientDialogController from '@app/components/navpatientheader/EditPatientDialogController';
import { ToastProvider } from '@app/providers/ToastProvider';
import { buildGlycemicRangesFromPreset } from '@app/core/glycemicRangesUtils';
import { GLYCEMIC_RANGE_OPTS } from '@app/components/clinic/PatientForm/SelectGlycemicRanges';
@@ -24,22 +25,18 @@ jest.mock('@app/core/hooks', () => ({
usePrevious: jest.fn(),
}));
+const baseClinicPatient = {
+ id: 'patient123',
+ fullName: 'John Doe',
+ birthDate: '2000-01-01',
+ permissions: { custodian: {} },
+};
+
const initialState = {
blip: {
selectedClinicId: 'clinic123',
currentPatientInViewId: 'patient123',
- clinics: {
- clinic123: {
- id: 'clinic123',
- mrnSettings: { required: false },
- },
- },
- allUsersMap: {
- patient123: {
- id: 'patient123',
- profile: { fullName: 'John Doe' },
- },
- },
+ clinics: { clinic123: { id: 'clinic123', mrnSettings: { required: false } } },
working: {
fetchingClinicMRNsForPatientFormValidation: { inProgress: false, completed: false, notification: null },
updatingClinicPatient: { inProgress: false, completed: false, notification: null },
@@ -48,69 +45,7 @@ const initialState = {
},
};
-const renderEditPatientDialog = (storeState = initialState) => {
- const reducer = (state = storeState, action) => state;
- const store = createStore(reducer, applyMiddleware(thunk));
-
- return render(
-
-
-
-
-
-
-
- );
-};
-
-describe('EditPatientDialog', () => {
- 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' },
- },
- },
- },
- },
- };
-
- renderEditPatientDialog(smartOnFhirState);
-
- 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', () => {
- renderEditPatientDialog(initialState);
-
- 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();
- });
-
+describe('EditPatientDialogController', () => {
// 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.
@@ -129,13 +64,17 @@ describe('EditPatientDialog', () => {
const store = mockStore(justCompletedUpdateState);
const onClose = jest.fn();
+ const testApi = { clinics: { getPatientFromClinic: jest.fn() } };
+
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
@@ -164,7 +103,7 @@ describe('EditPatientDialog', () => {
const IDLE_UPDATE = { inProgress: false, completed: null, notification: null };
const COMPLETED_UPDATE = { inProgress: false, completed: true, notification: null };
- const makeState = ({ savedRange, chartDataSize, updatingClinicPatient }) => ({
+ const makeState = ({ chartDataSize, updatingClinicPatient }) => ({
blip: {
...initialState.blip,
working: {
@@ -173,12 +112,7 @@ describe('EditPatientDialog', () => {
},
data: { metaData: { size: chartDataSize } },
clinics: {
- clinic123: {
- ...initialState.blip.clinics.clinic123,
- patients: {
- patient123: { id: 'patient123', fullName: 'John Doe', birthDate: '2000-01-01', glycemicRanges: savedRange },
- },
- },
+ clinic123: { ...initialState.blip.clinics.clinic123 },
},
},
});
@@ -186,13 +120,15 @@ describe('EditPatientDialog', () => {
const api = { clinics: { getPatientFromClinic: jest.fn(), updateClinicPatient: jest.fn() } };
const onClose = jest.fn();
- const ui = (store) => (
+ const ui = (store, clinicPatient) => (
-
-
-
-
-
+
+
+
+
+
+
+
);
@@ -204,58 +140,61 @@ describe('EditPatientDialog', () => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
});
+ const originalClinicPatient = { ...baseClinicPatient, glycemicRanges: RANGE_A };
+ const uneditedClinicPatient = { ...baseClinicPatient, glycemicRanges: RANGE_A };
+ const modifiedClinicPatient = { ...baseClinicPatient, glycemicRanges: RANGE_B };
+
it('does NOT clear the data worker for a demographic-only edit, even when chart data exists', async () => {
- const initialState = makeState({ savedRange: RANGE_A, chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE });
- const { rerender } = render(ui(mockStore(initialState)));
+ const { rerender } = render(ui(mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE })), originalClinicPatient));
await userEvent.type(screen.getByRole('textbox', { name: /Full Name/i }), ' Jr');
await userEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
- const completedStore = mockStore(makeState({ savedRange: RANGE_A, chartDataSize: 100, updatingClinicPatient: COMPLETED_UPDATE }));
- rerender(ui(completedStore));
+ const completedStore = mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: COMPLETED_UPDATE }));
+ rerender(ui(completedStore, uneditedClinicPatient));
expect(onClose).toHaveBeenCalled();
expect(cleared(completedStore)).toBe(false);
});
it('DOES clear the data worker when Target Range changed from the saved value and the patient has chart data', async () => {
- const { rerender } = render(ui(mockStore(makeState({ savedRange: RANGE_A, chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE }))));
+ const { rerender } = render(ui(mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE })), originalClinicPatient));
await userEvent.click(screen.getByLabelText('Target Range'));
await userEvent.click(screen.getByText(RANGE_B_LABEL));
await userEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
- const completedStore = mockStore(makeState({ savedRange: RANGE_A, chartDataSize: 100, updatingClinicPatient: COMPLETED_UPDATE }));
- rerender(ui(completedStore));
+ const completedStore = mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: COMPLETED_UPDATE }));
+ rerender(ui(completedStore, modifiedClinicPatient));
expect(onClose).toHaveBeenCalled();
expect(cleared(completedStore)).toBe(true);
});
it('clears on a SUBSEQUENT Target Range change — compares against the saved value, not the frozen original', async () => {
- const { rerender } = render(ui(mockStore(makeState({ savedRange: RANGE_A, chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE }))));
- rerender(ui(mockStore(makeState({ savedRange: RANGE_B, chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE }))));
+ const { rerender } = render(ui(mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE })), originalClinicPatient));
+ rerender(ui(mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: IDLE_UPDATE })), modifiedClinicPatient));
await userEvent.click(screen.getByLabelText('Target Range'));
await userEvent.click(screen.getByText(RANGE_A_LABEL));
await userEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
- const completedStore = mockStore(makeState({ savedRange: RANGE_B, chartDataSize: 100, updatingClinicPatient: COMPLETED_UPDATE }));
- rerender(ui(completedStore));
+ const completedStore = mockStore(makeState({ chartDataSize: 100, updatingClinicPatient: COMPLETED_UPDATE }));
+ rerender(ui(completedStore, modifiedClinicPatient));
expect(onClose).toHaveBeenCalled();
expect(cleared(completedStore)).toBe(true);
});
it('does NOT clear the data worker on a Target Range change for a no-data patient (would strand the loader)', async () => {
- const { rerender } = render(ui(mockStore(makeState({ savedRange: RANGE_A, chartDataSize: 0, updatingClinicPatient: IDLE_UPDATE }))));
+ const { rerender } = render(ui(mockStore(makeState({ chartDataSize: 0, updatingClinicPatient: IDLE_UPDATE })), originalClinicPatient));
await userEvent.click(screen.getByLabelText('Target Range'));
await userEvent.click(screen.getByText(RANGE_B_LABEL));
await userEvent.click(screen.getByRole('button', { name: 'Save Changes' }));
- const completedStore = mockStore(makeState({ savedRange: RANGE_A, chartDataSize: 0, updatingClinicPatient: COMPLETED_UPDATE }));
- rerender(ui(completedStore));
+ const completedStore = mockStore(makeState({ chartDataSize: 0, updatingClinicPatient: COMPLETED_UPDATE }));
+ rerender(ui(completedStore, uneditedClinicPatient));
expect(onClose).toHaveBeenCalled();
expect(cleared(completedStore)).toBe(false);
diff --git a/app/components/navpatientheader/EditPatientDialog.js b/app/components/clinic/EditPatientDialog.js
similarity index 50%
rename from app/components/navpatientheader/EditPatientDialog.js
rename to app/components/clinic/EditPatientDialog.js
index ab6a360d0b..ba99bcd2eb 100644
--- a/app/components/navpatientheader/EditPatientDialog.js
+++ b/app/components/clinic/EditPatientDialog.js
@@ -1,45 +1,38 @@
-import React, { useState, useEffect, useMemo, useRef } from 'react';
-import { useSelector, useDispatch } from 'react-redux';
+import React, { useState, useEffect, useMemo } from 'react';
+import { useSelector } from 'react-redux';
import { useIsFirstRender, usePrevious } from '../../core/hooks';
import { useTranslation } from 'react-i18next';
-import { selectClinicPatient, selectIsSmartOnFhirMode } from '../../core/selectors';
+import { selectIsSmartOnFhirMode } from '../../core/selectors';
import { Dialog, DialogActions, DialogContent, DialogTitle } from '../elements/Dialog';
import { MediumTitle } from '../elements/FontStyles';
import Button from '../elements/Button';
import PatientForm from '../clinic/PatientForm';
import noop from 'lodash/noop';
import keys from 'lodash/keys';
-import get from 'lodash/get';
-import isEqual from 'lodash/isEqual';
import { fieldsAreValid } from '../../core/forms';
import { patientSchema as validationSchema } from '../../core/clinicUtils';
-import { DEFAULT_GLYCEMIC_RANGES } from '../../core/glycemicRangesUtils';
-import { useToasts } from '../../providers/ToastProvider';
-import * as actions from '../../redux/actions';
+import { trackMetric } from '../../core/metricUtils';
+import useClinicMetricsPageName from '../../pages/clinicworkspace/useClinicMetricsPageName';
-const useUpdatingClinicPatientWorkingState = ({ onUpdateSuccess = noop }) => {
- const { t } = useTranslation();
+const PATIENT_FORM_SEARCH_DEBOUNCE_MS = 600;
+
+const useUpdatingClinicPatientWorkingState = ({
+ onEditSuccess = noop,
+ onEditFailure = noop,
+}) => {
const updatingClinicPatient = useSelector((state) => state.blip.working.updatingClinicPatient);
- const { set: setToast } = useToasts();
- const { inProgress, completed, notification } = updatingClinicPatient;
+ const { inProgress, completed } = updatingClinicPatient;
const prevInProgress = usePrevious(inProgress);
const isFirstRender = useIsFirstRender();
useEffect(() => {
if (!isFirstRender && !inProgress && prevInProgress !== false) {
if (completed) {
- onUpdateSuccess();
- setToast({
- message: t('You have successfully updated a patient.'),
- variant: 'success',
- });
+ onEditSuccess();
}
if (completed === false) {
- setToast({
- message: get(notification, 'message'),
- variant: 'danger',
- });
+ onEditFailure();
}
}
}, [isFirstRender, inProgress, prevInProgress, completed]);
@@ -47,27 +40,22 @@ const useUpdatingClinicPatientWorkingState = ({ onUpdateSuccess = noop }) => {
return updatingClinicPatient;
};
-const PATIENT_FORM_SEARCH_DEBOUNCE_MS = 600;
-
const EditPatientDialog = ({
api,
- trackMetric,
- isOpen,
+ clinicPatient,
+ isOpen = false,
onClose = noop,
+ onEditConfirm = noop,
+ onEditSuccess = noop,
+ onEditFailure = noop,
}) => {
const { t } = useTranslation();
- const dispatch = useDispatch();
-
- const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
- const currentPatientInViewId = useSelector(state => state.blip.currentPatientInViewId);
- const clinicPatient = useSelector(state => selectClinicPatient(state));
+ const pageName = useClinicMetricsPageName();
+ const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const clinic = useSelector(state => state.blip.clinics?.[selectedClinicId]);
const isSmartOnFhir = useSelector(selectIsSmartOnFhirMode);
- const mrnSettings = useMemo(() => clinic?.mrnSettings ?? {}, [clinic?.mrnSettings]);
- const existingMRNs = useSelector(state => state.blip.clinicMRNsForPatientFormValidation)?.filter(mrn => mrn !== clinicPatient?.mrn) || [];
-
- const hasChartData = useSelector(state => (state.blip.data?.metaData?.size || 0) > 0);
+ const [patientFormContext, setPatientFormContext] = useState();
// In smart-on-fhir mode, identity fields are sourced from the EHR and locked.
const disabledFields = useMemo(
@@ -75,45 +63,26 @@ const EditPatientDialog = ({
[isSmartOnFhir]
);
- // Captured at submit time: whether this edit needs the chart data reprocessed.
- const shouldClearDataRef = useRef(false);
+ const mrnSettings = useMemo(() => clinic?.mrnSettings ?? {}, [clinic?.mrnSettings]);
+ const existingMRNs = useSelector(state => state.blip.clinicMRNsForPatientFormValidation)?.filter(mrn => mrn !== clinicPatient?.mrn) || [];
- const onUpdateSuccess = () => {
- // 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.
- if (!isOpen) return;
+ const updatingClinicPatient = useUpdatingClinicPatientWorkingState({ onEditSuccess, onEditFailure });
- onClose();
+ const disabled = !patientFormContext || !fieldsAreValid(
+ keys(patientFormContext?.values),
+ validationSchema({ mrnSettings, existingMRNs }), patientFormContext?.values
+ );
- if (shouldClearDataRef.current) {
- dispatch(actions.worker.dataWorkerRemoveDataRequest(null, currentPatientInViewId));
- shouldClearDataRef.current = false;
- }
+ const handlePatientFormChange = (formikContext) => {
+ setPatientFormContext({ ...formikContext });
};
- const updatingClinicPatient = useUpdatingClinicPatientWorkingState({ onUpdateSuccess });
-
- const [patientFormContext, setPatientFormContext] = useState();
+ const handleSubmit = () => {
+ onEditConfirm(patientFormContext); // notify parent
- const handleEditPatientConfirm = () => {
- // 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(patientFormContext?.values?.glycemicRanges, savedRange);
- shouldClearDataRef.current = targetRangeChanged && hasChartData;
patientFormContext?.handleSubmit();
};
- const handlePatientFormChange = (formikContext) => {
- setPatientFormContext({ ...formikContext });
- };
-
- if (!currentPatientInViewId || !selectedClinicId) return null;
-
return (