diff --git a/app/components/clinic/PatientForm.js b/app/components/clinic/PatientForm.js index 306f098898..4342731e81 100644 --- a/app/components/clinic/PatientForm.js +++ b/app/components/clinic/PatientForm.js @@ -227,7 +227,9 @@ export const PatientForm = (props) => { if (!isFirstRender && !inProgress) { if (completed) { + // Close the resend email modal and refetch patient details to update the connection status setShowResendDexcomConnectRequest(false); + fetchPatientDetails(); setToast({ message: successMessage, @@ -309,7 +311,7 @@ export const PatientForm = (props) => { // Pull the patient on load to ensure the most recent dexcom connection state is made available useEffect(() => { - if ((action === 'edit') && selectedClinicId && patient?.id) dispatch(actions.async.fetchPatientFromClinic.bind(null, api, selectedClinicId, patient.id)()) + if ((action === 'edit') && selectedClinicId && patient?.id) fetchPatientDetails(); }, []); useEffect(() => { @@ -329,6 +331,10 @@ export const PatientForm = (props) => { dispatch(actions.async.sendPatientDexcomConnectRequest(api, selectedClinicId, patient.id)); } + function fetchPatientDetails() { + dispatch(actions.async.fetchPatientFromClinic(api, selectedClinicId, patient.id)); + } + function renderRegionalNote() { return ( diff --git a/app/core/clinicUtils.js b/app/core/clinicUtils.js index f66bc4ab25..eda0a22cc4 100644 --- a/app/core/clinicUtils.js +++ b/app/core/clinicUtils.js @@ -397,12 +397,11 @@ export const clinicPatientTagSchema = yup.object().shape({ export const patientSchema = config => { let mrnSchema = yup .string() - .matches(/^$|^[A-Z0-9]{4,25}$/, () => ( + .matches(/^$|^[A-Z0-9]{0,25}$/, () => (
{t('Patient\'s MRN is invalid. MRN must meet the following criteria:')} diff --git a/app/core/hooks.js b/app/core/hooks.js index df04bb9ca3..428a82c3fa 100644 --- a/app/core/hooks.js +++ b/app/core/hooks.js @@ -183,3 +183,33 @@ export const useIsFirstRender = () => { return isFirstRenderRef.current; }; + +/** + * Disables triggering the increment/decrememnt inputs on active number inputs while scrolling + * c.f https://stackoverflow.com/a/56250826 + * + * There are a few different approaches, but this seems to be the best. + * Some approaches blur for a frame then refocus, which causes the outline to flicker, and could potentially trigger onBlur validations + * Some approaches simple cancel out mousewheel events on active number inputs, but this means that a user can't scroll out of an active, hovered input + * This approach has a very small flicker on the increment/decrement controls while scrolling on an active number input, but it seems to be the most minor of compromises + * + * Note: it may be prudent to at some point stop using number inputs altogther, switch to using + * plain text that mimic number fields without the scroll handlers built in: + * + * https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/ + */ +export const useDisableScrollOnNumberInput = () => { + useEffect(() => { + function handleScroll(e) { + if (e.target.tagName.toLowerCase() === 'input' + && (e.target.type === 'number') + && (e.target === document.activeElement) + && !e.target.readOnly + ) { + e.target.readOnly = true; + setTimeout(function(el){ el.readOnly = false; }, 0, e.target); + } + } + document.addEventListener('wheel', function(e){ handleScroll(e); }); + }, []); +}; diff --git a/app/pages/dashboard/TideDashboard.js b/app/pages/dashboard/TideDashboard.js index 0ef87e14ae..38834b3a3a 100644 --- a/app/pages/dashboard/TideDashboard.js +++ b/app/pages/dashboard/TideDashboard.js @@ -303,10 +303,12 @@ const TideDashboardSection = React.memo(props => { {patient?.fullName} @@ -435,12 +437,15 @@ const TideDashboardSection = React.memo(props => { const renderDexcomConnectionStatus = useCallback(({ patient }) => { const dexcomDataSource = find(patient?.dataSources, { providerName: 'dexcom' }); + const dexcomAuthInviteExpired = dexcomDataSource?.expirationTime < moment.utc().toISOString(); let dexcomConnectState; if (dexcomDataSource) { dexcomConnectState = includes(keys(dexcomConnectStateUI), dexcomDataSource?.state) ? dexcomDataSource.state : 'unknown'; + + if (includes(['pending', 'pendingReconnect'], dexcomConnectState) && dexcomAuthInviteExpired) dexcomConnectState = 'pendingExpired'; } else { dexcomConnectState = 'noPendingConnections'; } diff --git a/app/pages/prescription/therapySettingsFormStep.js b/app/pages/prescription/therapySettingsFormStep.js index e3e9c4cadf..296a4dda3c 100644 --- a/app/pages/prescription/therapySettingsFormStep.js +++ b/app/pages/prescription/therapySettingsFormStep.js @@ -12,7 +12,7 @@ import { default as _values } from 'lodash/values'; import { getFieldError, getThresholdWarning, onChangeWithDependantFields } from '../../core/forms'; import utils from '../../core/utils'; -import { useInitialFocusedInput } from '../../core/hooks'; +import { useInitialFocusedInput, useDisableScrollOnNumberInput } from '../../core/hooks'; import { Paragraph2, Body2, Headline, Title } from '../../components/elements/FontStyles'; import RadioGroup from '../../components/elements/RadioGroup'; import PopoverLabel from '../../components/elements/PopoverLabel'; @@ -554,6 +554,7 @@ export const InsulinSettings = props => { InsulinSettings.propTypes = fieldsetPropTypes; export const TherapySettings = withTranslation()(props => { + useDisableScrollOnNumberInput(); const formikContext = useFormikContext(); const { diff --git a/app/redux/reducers/misc.js b/app/redux/reducers/misc.js index ed9430b96d..59b540283a 100644 --- a/app/redux/reducers/misc.js +++ b/app/redux/reducers/misc.js @@ -1149,8 +1149,9 @@ export const tideDashboardPatients = (state = initialState.tideDashboardPatients case types.FETCH_TIDE_DASHBOARD_PATIENTS_SUCCESS: return action?.payload?.results || initialState.tideDashboardPatients; case types.UPDATE_CLINIC_PATIENT_SUCCESS: + case types.FETCH_PATIENT_FROM_CLINIC_SUCCESS: const patient = _.get(action.payload, 'patient'); - const patientId = _.get(action.payload, 'patientId'); + const patientId = patient.id; const newResults = _.reduce(state.results, (results, value, key) => { const matchingPatientIndex = _.findIndex(value, ({ patient }) => patient?.id === patientId); diff --git a/package.json b/package.json index 5585ec2cd0..4e54b515af 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "terser-webpack-plugin": "5.3.9", "theme-ui": "0.16.1", "tideline": "1.30.0", - "tidepool-platform-client": "0.61.0-web-3178-tide-connection-status.1", + "tidepool-platform-client": "0.61.0", "tidepool-standard-action": "0.1.1", "ua-parser-js": "1.0.36", "url-loader": "4.1.1", diff --git a/test/fixtures/mockTideDashboardPatients.json b/test/fixtures/mockTideDashboardPatients.json index b2432acf78..1b279c986e 100644 --- a/test/fixtures/mockTideDashboardPatients.json +++ b/test/fixtures/mockTideDashboardPatients.json @@ -590,9 +590,7 @@ "63d811e11710f10c0422ce6d", "645a9c5dd59edded4e573495" ], - "dataSources": [ - { "providerName": "dexcom", "state": "noPendingConnections" } - ] + "dataSources": [] } }, { @@ -615,7 +613,7 @@ "646f7ed708e23bc18d91caa4" ], "dataSources": [ - { "providerName": "dexcom", "state": "pendingExpired" } + { "providerName": "dexcom", "state": "pending", "expirationTime": "2024-11-17T15:17:20.159+00:00" } ] } }, @@ -640,7 +638,7 @@ "646f7f0408e23bc18d91caa7" ], "dataSources": [ - { "providerName": "dexcom", "state": "unknown" } + { "providerName": "dexcom", "state": "pending" } ] } } diff --git a/test/unit/pages/ClinicPatients.test.js b/test/unit/pages/ClinicPatients.test.js index 7fd321d5dd..24fd61cacb 100644 --- a/test/unit/pages/ClinicPatients.test.js +++ b/test/unit/pages/ClinicPatients.test.js @@ -797,10 +797,10 @@ describe('ClinicPatients', () => { expect(dialog().find('Button#addPatientConfirm').prop('disabled')).to.be.true; expect(patientForm().find('input[name="mrn"]').prop('value')).to.equal(''); - patientForm().find('input[name="mrn"]').simulate('change', { persist: noop, target: { name: 'mrn', value: 'mr2' } }); - expect(patientForm().find('input[name="mrn"]').prop('value')).to.equal('MR2'); + patientForm().find('input[name="mrn"]').simulate('change', { persist: noop, target: { name: 'mrn', value: 'm' } }); + expect(patientForm().find('input[name="mrn"]').prop('value')).to.equal('M'); - expect(dialog().find('Button#addPatientConfirm').prop('disabled')).to.be.true; + expect(dialog().find('Button#addPatientConfirm').prop('disabled')).to.be.false; patientForm().find('input[name="mrn"]').simulate('change', { persist: noop, target: { name: 'mrn', value: 'mrn876thiswillexceedthelengthlimit' } }); expect(patientForm().find('input[name="mrn"]').prop('value')).to.equal('MRN876THISWILLEXCEEDTHELENGTHLIMIT'); diff --git a/test/unit/pages/TideDashboard.test.js b/test/unit/pages/TideDashboard.test.js index 6d1bdb61ad..ff4f260701 100644 --- a/test/unit/pages/TideDashboard.test.js +++ b/test/unit/pages/TideDashboard.test.js @@ -660,6 +660,19 @@ describe('TideDashboard', () => { expect(getTableRow(6, 0).find('th').at(2).text()).contains('Days Since Last Data'); expect(getTableRow(6, 1).find('td').at(1).text()).contains('200'); + + // Verify that various connection statuses are rendering correctly + expect(getTableRow(6, 2).find('th').at(0).text()).contains('Willie Gambles'); + expect(getTableRow(6, 2).find('td').at(0).text()).contains('Invite Sent'); + + expect(getTableRow(6, 3).find('th').at(0).text()).contains('Denys Ickov'); + expect(getTableRow(6, 3).find('td').at(0).text()).contains('Patient Disconnected'); + + expect(getTableRow(6, 4).find('th').at(0).text()).contains('Johna Slatcher'); + expect(getTableRow(6, 4).find('td').at(0).text()).contains('No Pending Connections'); + + expect(getTableRow(6, 5).find('th').at(0).text()).contains('Emelda Stangoe'); + expect(getTableRow(6, 5).find('td').at(0).text()).contains('Invite Expired'); }); it('should show empty text for a section without results', () => { diff --git a/test/unit/redux/reducers/tideDashboardPatients.test.js b/test/unit/redux/reducers/tideDashboardPatients.test.js index 43306aeb6a..6784bb5668 100644 --- a/test/unit/redux/reducers/tideDashboardPatients.test.js +++ b/test/unit/redux/reducers/tideDashboardPatients.test.js @@ -52,7 +52,7 @@ describe('tideDashboardPatients', () => { }); describe('updateClinicPatientSuccess', () => { - it('should set state to initial empty state', () => { + it('should update matching patient in state', () => { const patients = { results: { 'foo': [ { patient: {id: 'bar' } }, { patient: { id: 'baz'} } @@ -69,4 +69,23 @@ describe('tideDashboardPatients', () => { ] } }); }); }); + + describe('fetchPatientFromClinicSuccess', () => { + it('should update matching patient in state', () => { + const patients = { results: { 'foo': [ + { patient: {id: 'bar' } }, + { patient: { id: 'baz'} } + ] } }; + + const updatedPatient = { id: 'baz', updated: true } + let initialStateForTest = patients; + let action = actions.sync.fetchPatientFromClinicSuccess('clinicId123', updatedPatient); + let state = reducer(initialStateForTest, action); + + expect(state).to.eql({ results: { 'foo': [ + { patient: {id: 'bar' } }, + { patient: { id: 'baz', updated: true } } + ] } }); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index 21a878c3de..d086fbb71d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7429,7 +7429,7 @@ __metadata: terser-webpack-plugin: 5.3.9 theme-ui: 0.16.1 tideline: 1.30.0 - tidepool-platform-client: 0.61.0-web-3178-tide-connection-status.1 + tidepool-platform-client: 0.61.0 tidepool-standard-action: 0.1.1 ua-parser-js: 1.0.36 url-loader: 4.1.1 @@ -20184,15 +20184,15 @@ __metadata: languageName: node linkType: hard -"tidepool-platform-client@npm:0.61.0-web-3178-tide-connection-status.1": - version: 0.61.0-web-3178-tide-connection-status.1 - resolution: "tidepool-platform-client@npm:0.61.0-web-3178-tide-connection-status.1" +"tidepool-platform-client@npm:0.61.0": + version: 0.61.0 + resolution: "tidepool-platform-client@npm:0.61.0" dependencies: async: 0.9.0 lodash: 4.17.21 superagent: 5.2.2 uuid: 3.1.0 - checksum: 0b7254503ba7ec9c880da9e47e69273a24d250a5b7ff2f4b15d424bc59780631d11a73e5024a4c348cb6e3b8985b4591e402217d7f8c9c5cb72a3e931ab20756 + checksum: 96e221a65b7a003523e03d9ba357b0c1aa371b779ffd363c325ba015b893462955188662587b502193aaf269d6784577c8d24b8c764ba4ad759d4b0ba511c8ae languageName: node linkType: hard