From 8c9dc49136be368e66cce055b27bff98ac48d425 Mon Sep 17 00:00:00 2001 From: clintonium-119 Date: Thu, 15 Aug 2024 14:34:03 -0400 Subject: [PATCH 1/3] Allow skipping calculator step for select pump devices --- app/pages/prescription/PrescriptionForm.js | 275 +++++++++--------- .../prescription/prescriptionFormConstants.js | 31 +- app/pages/prescription/profileFormSteps.js | 4 +- .../prescription/PrescriptionForm.test.js | 52 +++- .../prescriptionFormConstants.test.js | 14 +- 5 files changed, 222 insertions(+), 154 deletions(-) diff --git a/app/pages/prescription/PrescriptionForm.js b/app/pages/prescription/PrescriptionForm.js index eb4044debb..dc96d4c501 100644 --- a/app/pages/prescription/PrescriptionForm.js +++ b/app/pages/prescription/PrescriptionForm.js @@ -8,6 +8,7 @@ import moment from 'moment'; import { FastField, withFormik, useFormikContext } from 'formik'; import { PersistFormikValues } from 'formik-persist-values'; import each from 'lodash/each'; +import every from 'lodash/every'; import find from 'lodash/find'; import forEach from 'lodash/forEach'; import get from 'lodash/get'; @@ -55,6 +56,7 @@ import { defaultUnits, deviceIdMap, prescriptionStateOptions, + pumpDeviceOptions, stepValidationFields, validCountryCodes, } from './prescriptionFormConstants'; @@ -284,7 +286,7 @@ export const PrescriptionForm = props => { const selectedClinicId = useSelector((state) => state.blip.selectedClinicId); const stepperId = 'prescription-form-steps'; const bgUnits = get(values, 'initialSettings.bloodGlucoseUnits', defaultUnits.bloodGlucose); - const pumpId = get(values, 'initialSettings.pumpId', deviceIdMap.omnipodHorizon); + const pumpId = get(values, 'initialSettings.pumpId', deviceIdMap.palmtree); const pump = find(devices.pumps, { id: pumpId }); const prescriptionState = get(prescription, 'state', 'draft'); const prescriptionStates = keyBy(prescriptionStateOptions, 'value'); @@ -332,107 +334,10 @@ export const PrescriptionForm = props => { const [initialFocusedInput, setInitialFocusedInput] = useState(); const [singleStepEditValues, setSingleStepEditValues] = useState(values); const isSingleStepEdit = !!pendingStep.length; - const isLastStep = activeStep === stepValidationFields.length - 1; + const validationFields = [ ...stepValidationFields ]; + const isLastStep = activeStep === validationFields.length - 1; const isNewPrescription = isEmpty(get(values, 'id')); - useEffect(() => { - // Determine the latest incomplete step, and default to starting there - if (isEditable && (isUndefined(activeStep) || isUndefined(activeSubStep))) { - let firstInvalidStep; - let firstInvalidSubStep; - let currentStep = 0; - let currentSubStep = 0; - - while (isUndefined(firstInvalidStep) && currentStep < stepValidationFields.length) { - while (currentSubStep < stepValidationFields[currentStep].length) { - if (!fieldsAreValid(stepValidationFields[currentStep][currentSubStep], schema, values)) { - firstInvalidStep = currentStep; - firstInvalidSubStep = currentSubStep; - break; - } - currentSubStep++ - } - - currentStep++; - currentSubStep = 0; - } - - setActiveStep(isInteger(firstInvalidStep) ? firstInvalidStep : 4); - setActiveSubStep(isInteger(firstInvalidSubStep) ? firstInvalidSubStep : 0); - } - - // When a user comes to this component initially, without the active step and subStep set by the - // Stepper component in the url, or when editing an existing prescription, - // we delete any persisted state from localStorage. - if (status.isPrescriptionEditFlow || (get(localStorage, storageKey) && activeStepsParam === null)) { - delete localStorage[storageKey]; - } - - // Only use the localStorage persistence for new prescriptions - not while editing an existing one. - setFormPersistReady(!prescription); - }, []); - - // Save whether or not we are editing a single step to the formik form status for easy reference - useEffect(() => { - setStatus({ - ...status, - isSingleStepEdit, - }); - }, [isSingleStepEdit]) - - // Save the hydrated localStorage values to the formik form status for easy reference - useEffect(() => { - if (formPersistReady) setStatus({ - ...status, - hydratedValues: JSON.parse(get(localStorage, storageKey, JSON.stringify(status.hydratedValues))), - }); - }, [formPersistReady]); - - // Handle changes to stepper async state for completed prescription creation and revision updates - useEffect(() => { - const isRevision = !!get(values, 'id'); - const isDraft = get(values, 'state') === 'draft'; - const { inProgress, completed, notification, prescriptionId } = isRevision ? creatingPrescriptionRevision : creatingPrescription; - - if (prescriptionId) setFieldValue('id', prescriptionId); - - if (!isFirstRender && !inProgress) { - if (completed) { - setStepAsyncState(asyncStates.completed); - if (isLastStep) { - - let messageAction = isRevision ? t('updated') : t('created'); - if (isPrescriber) messageAction = t('finalized and sent'); - - setToast({ - message: t('You have successfully {{messageAction}} a Tidepool Loop prescription.', { messageAction }), - variant: 'success', - }); - - history.push('/clinic-workspace/prescriptions'); - } - } - - if (completed === false) { - setToast({ - message: get(notification, 'message'), - variant: 'danger', - }); - - setStepAsyncState(asyncStates.failed); - } - } - }, [creatingPrescription, creatingPrescriptionRevision]); - - useEffect(() => { - if (stepAsyncState.complete === false) { - // Allow resubmission of form after a second - setTimeout(() => { - setStepAsyncState(asyncStates.initial); - }, 1000); - } - }, [stepAsyncState.complete]); - const handlers = { activeStepUpdate: ([step, subStep], fromStep = [], initialFocusedInput) => { setActiveStep(step); @@ -476,7 +381,7 @@ export const PrescriptionForm = props => { // entered in the later steps. if (!isLastStep) { const emptyFieldsInFutureSteps = remove( - flattenDeep(slice(stepValidationFields, activeStep + 1)), + flattenDeep(slice(validationFields, activeStep + 1)), fieldPath => { const value = get(values, fieldPath); @@ -544,6 +449,143 @@ export const PrescriptionForm = props => { const subStepProps = subSteps => map(subSteps, subStep => stepProps(subStep)); + const steps = [ + { + ...accountFormStepsProps, + onComplete: isSingleStepEdit ? noop : handlers.stepSubmit, + asyncState: isSingleStepEdit ? null : stepAsyncState, + subSteps: subStepProps(accountFormStepsProps.subSteps), + }, + { + ...profileFormStepsProps, + onComplete: isSingleStepEdit ? noop : handlers.stepSubmit, + asyncState: isSingleStepEdit ? null : stepAsyncState, + subSteps: subStepProps(profileFormStepsProps.subSteps), + }, + { + ...settingsCalculatorFormStepsProps, + onComplete: handlers.stepSubmit, + asyncState: stepAsyncState, + subSteps: subStepProps(settingsCalculatorFormStepsProps.subSteps), + }, + { + ...stepProps(therapySettingsFormStepProps), + onComplete: isSingleStepEdit ? handlers.singleStepEditComplete : handlers.stepSubmit, + asyncState: isSingleStepEdit ? null : stepAsyncState, + }, + { + ...reviewFormStepProps, + onComplete: handlers.stepSubmit, + asyncState: stepAsyncState, + }, + ]; + + // Remove calculator step if selected pump, or all available pump options are set to skip aace calculator + const pumpDevices = pumpDeviceOptions(devices); + const skipCalculator = !!(pumpDevices.length && every(pumpDevices, { skipCalculator: true })) || !!find(devices, { value: pumpId })?.skipCalculator; + if (skipCalculator) { + validationFields.splice(2, 1); + steps.splice(2, 1); + } + + useEffect(() => { + // Determine the latest incomplete step, and default to starting there + if (isEditable && (isUndefined(activeStep) || isUndefined(activeSubStep))) { + let firstInvalidStep; + let firstInvalidSubStep; + let currentStep = 0; + let currentSubStep = 0; + + while (isUndefined(firstInvalidStep) && currentStep < validationFields.length) { + while (currentSubStep < validationFields[currentStep].length) { + if (!fieldsAreValid(validationFields[currentStep][currentSubStep], schema, values)) { + firstInvalidStep = currentStep; + firstInvalidSubStep = currentSubStep; + break; + } + currentSubStep++ + } + + currentStep++; + currentSubStep = 0; + } + + setActiveStep(isInteger(firstInvalidStep) ? firstInvalidStep : steps.length - 1); + setActiveSubStep(isInteger(firstInvalidSubStep) ? firstInvalidSubStep : 0); + } + + // When a user comes to this component initially, without the active step and subStep set by the + // Stepper component in the url, or when editing an existing prescription, + // we delete any persisted state from localStorage. + if (status.isPrescriptionEditFlow || (get(localStorage, storageKey) && activeStepsParam === null)) { + delete localStorage[storageKey]; + } + + // Only use the localStorage persistence for new prescriptions - not while editing an existing one. + setFormPersistReady(!prescription); + }, []); + + // Save whether or not we are editing a single step to the formik form status for easy reference + useEffect(() => { + setStatus({ + ...status, + isSingleStepEdit, + }); + }, [isSingleStepEdit]) + + // Save the hydrated localStorage values to the formik form status for easy reference + useEffect(() => { + if (formPersistReady) setStatus({ + ...status, + hydratedValues: JSON.parse(get(localStorage, storageKey, JSON.stringify(status.hydratedValues))), + }); + }, [formPersistReady]); + + // Handle changes to stepper async state for completed prescription creation and revision updates + useEffect(() => { + const isRevision = !!get(values, 'id'); + const isDraft = get(values, 'state') === 'draft'; + const { inProgress, completed, notification, prescriptionId } = isRevision ? creatingPrescriptionRevision : creatingPrescription; + + if (prescriptionId) setFieldValue('id', prescriptionId); + + if (!isFirstRender && !inProgress) { + if (completed) { + setStepAsyncState(asyncStates.completed); + if (isLastStep) { + + let messageAction = isRevision ? t('updated') : t('created'); + if (isPrescriber) messageAction = t('finalized and sent'); + + setToast({ + message: t('You have successfully {{messageAction}} a Tidepool Loop prescription.', { messageAction }), + variant: 'success', + }); + + history.push('/clinic-workspace/prescriptions'); + } + } + + if (completed === false) { + setToast({ + message: get(notification, 'message'), + variant: 'danger', + }); + + setStepAsyncState(asyncStates.failed); + } + } + }, [creatingPrescription, creatingPrescriptionRevision]); + + useEffect(() => { + if (stepAsyncState.complete === false) { + // Allow resubmission of form after a second + setTimeout(() => { + setStepAsyncState(asyncStates.initial); + }, 1000); + } + }, [stepAsyncState.complete]); + const stepperProps = { activeStep, activeSubStep, @@ -563,36 +605,7 @@ export const PrescriptionForm = props => { log('Step to', newStep.join(',')); }, - steps: [ - { - ...accountFormStepsProps, - onComplete: isSingleStepEdit ? noop : handlers.stepSubmit, - asyncState: isSingleStepEdit ? null : stepAsyncState, - subSteps: subStepProps(accountFormStepsProps.subSteps), - }, - { - ...profileFormStepsProps, - onComplete: isSingleStepEdit ? noop : handlers.stepSubmit, - asyncState: isSingleStepEdit ? null : stepAsyncState, - subSteps: subStepProps(profileFormStepsProps.subSteps), - }, - { - ...settingsCalculatorFormStepsProps, - onComplete: handlers.stepSubmit, - asyncState: stepAsyncState, - subSteps: subStepProps(settingsCalculatorFormStepsProps.subSteps), - }, - { - ...stepProps(therapySettingsFormStepProps), - onComplete: isSingleStepEdit ? handlers.singleStepEditComplete : handlers.stepSubmit, - asyncState: isSingleStepEdit ? null : stepAsyncState, - }, - { - ...reviewFormStepProps, - onComplete: handlers.stepSubmit, - asyncState: stepAsyncState, - }, - ], + steps, themeProps: { wrapper: { padding: 4, diff --git a/app/pages/prescription/prescriptionFormConstants.js b/app/pages/prescription/prescriptionFormConstants.js index 4ed51dfb3a..ef208871be 100644 --- a/app/pages/prescription/prescriptionFormConstants.js +++ b/app/pages/prescription/prescriptionFormConstants.js @@ -36,17 +36,22 @@ export const validDeviceIds = { ], }; -export const deviceExtraInfo = { - [deviceIdMap.dexcomG6]: ( - - Find information on how to prescribe Dexcom G6 sensors and transmitters and more here. - - ), - [deviceIdMap.palmtree]: ( - - Find information on how to prescribe Palmtree products here. - - ), +export const deviceDetails = { + [deviceIdMap.dexcomG6]: { + description: ( + + Find information on how to prescribe Dexcom G6 sensors and transmitters and more here. + + ), + }, + [deviceIdMap.palmtree]: { + description: ( + + Find information on how to prescribe Palmtree products here. + + ), + skipCalculator: true, + }, }; export const pumpDeviceOptions = ({ pumps } = {}) => map( @@ -54,7 +59,7 @@ export const pumpDeviceOptions = ({ pumps } = {}) => map( pump => ({ value: pump.id, label: t('{{displayName}}', { displayName: pump.displayName }), - extraInfo: deviceExtraInfo[pump.id] || null, + ...(deviceDetails[pump.id] || {}), }), ); @@ -63,7 +68,7 @@ export const cgmDeviceOptions = ({ cgms } = {}) => map( cgm => ({ value: cgm.id, label: t('{{displayName}}', { displayName: cgm.displayName }), - extraInfo: deviceExtraInfo[cgm.id] || null, + ...(deviceDetails[cgm.id] || {}), }), ); diff --git a/app/pages/prescription/profileFormSteps.js b/app/pages/prescription/profileFormSteps.js index af26525f29..e430bd3764 100644 --- a/app/pages/prescription/profileFormSteps.js +++ b/app/pages/prescription/profileFormSteps.js @@ -168,7 +168,7 @@ export const PatientDevices = withTranslation()(props => { }} {...checkboxStyles} /> - {device.extraInfo} + {device.description} ))} @@ -187,7 +187,7 @@ export const PatientDevices = withTranslation()(props => { error={getFieldError('initialSettings.cgmId', formikContext)} {...checkboxStyles} /> - {device.extraInfo} + {device.description} ))} diff --git a/test/unit/pages/prescription/PrescriptionForm.test.js b/test/unit/pages/prescription/PrescriptionForm.test.js index 0fc122d864..2dc4e81f88 100644 --- a/test/unit/pages/prescription/PrescriptionForm.test.js +++ b/test/unit/pages/prescription/PrescriptionForm.test.js @@ -18,6 +18,7 @@ import { import { ToastProvider } from '../../../../app/providers/ToastProvider'; import LDClientMock from '../../../fixtures/LDClientMock'; +import { deviceIdMap } from '../../../../app/pages/prescription/prescriptionFormConstants'; /* global chai */ /* global sinon */ @@ -85,7 +86,7 @@ describe('PrescriptionForm', () => { wrapper = mount( - + ); @@ -103,6 +104,23 @@ describe('PrescriptionForm', () => { }); it('should render the form steps', () => { + const nonSkippedPumpProps = { + ...defaultProps, + devices: { + cgm: [{ id: deviceIdMap.dexcomG6 }], + pumps: [{ id: 'somedevice' }], + }, + }; + + const Element = withFormik(prescriptionForm())(formikProps => ); + wrapper = mount( + + + + + + ); + const stepper = wrapper.find('#prescription-form-steps').hostNodes(); expect(stepper).to.have.length(1); @@ -118,6 +136,38 @@ describe('PrescriptionForm', () => { expect(steps.at(4).find('.MuiStepLabel-label').hostNodes().text()).to.equal('Review and Save Prescription'); }); + it('should not render the calculator form steps when using a palmtree pump', () => { + const skippedPumpProps = { + ...defaultProps, + devices: { + cgm: [{ id: deviceIdMap.dexcomG6 }], + pumps: [{ id: deviceIdMap.palmtree }], + }, + }; + + const Element = withFormik(prescriptionForm())(formikProps => ); + wrapper = mount( + + + + + + ); + + const stepper = wrapper.find('#prescription-form-steps').hostNodes(); + expect(stepper).to.have.length(1); + + const steps = stepper.find('.MuiStep-root'); + expect(steps).to.have.length(4); + + expect(steps.at(0).find('.MuiStepLabel-label').hostNodes().text()).to.equal('Create Patient Account'); + expect(steps.at(0).hasClass('active')).to.be.true; + + expect(steps.at(1).find('.MuiStepLabel-label').hostNodes().text()).to.equal('Complete Patient Profile'); + expect(steps.at(2).find('.MuiStepLabel-label').hostNodes().text()).to.equal('Enter Therapy Settings'); + expect(steps.at(3).find('.MuiStepLabel-label').hostNodes().text()).to.equal('Review and Save Prescription'); + }); + it('should render the form actions, with only the `next` button on the first step', () => { const actions = wrapper.find('.step-actions').hostNodes(); expect(actions).to.have.length(1); diff --git a/test/unit/pages/prescription/prescriptionFormConstants.test.js b/test/unit/pages/prescription/prescriptionFormConstants.test.js index 738a2d7fa2..8ba3e1b225 100644 --- a/test/unit/pages/prescription/prescriptionFormConstants.test.js +++ b/test/unit/pages/prescription/prescriptionFormConstants.test.js @@ -15,7 +15,7 @@ const devices = { pumps: [{ id: prescriptionFormConstants.deviceIdMap.palmtree }], }; -describe('prescriptionFormConstants', function() { +describe.only('prescriptionFormConstants', function() { it('should export the `dateFormat`', function() { expect(prescriptionFormConstants.dateFormat).to.equal('YYYY-MM-DD'); }); @@ -65,13 +65,13 @@ describe('prescriptionFormConstants', function() { }); it('should export a JSX element for extra info about each device', () => { - expect(prescriptionFormConstants.deviceExtraInfo).to.be.an('object').and.have.keys([ + expect(prescriptionFormConstants.deviceDetails).to.be.an('object').and.have.keys([ prescriptionFormConstants.deviceIdMap.dexcomG6, prescriptionFormConstants.deviceIdMap.palmtree, ]); - _.each(prescriptionFormConstants.deviceExtraInfo, info => { - expect(info).to.be.an('object'); - expect(info.props).to.be.an('object').and.have.keys(['children']); + _.each(prescriptionFormConstants.deviceDetails, info => { + expect(info.description).to.be.an('object'); + expect(info.description.props).to.be.an('object').and.have.keys(['children']); }); }); @@ -85,7 +85,7 @@ describe('prescriptionFormConstants', function() { _.each(pumpDeviceOptions, device => { expect(device.value).to.be.a('string'); expect(device.label).to.be.a('string'); - expect(device.extraInfo).to.be.an('object'); + expect(device.description).to.be.an('object'); }) }); @@ -99,7 +99,7 @@ describe('prescriptionFormConstants', function() { _.each(cgmDeviceOptions, device => { expect(device.value).to.be.a('string'); expect(device.label).to.be.a('string'); - expect(device.extraInfo).to.be.an('object'); + expect(device.description).to.be.an('object'); }) }); From 9080108291004dcb8fc97b13d5365e01cd8b2ce6 Mon Sep 17 00:00:00 2001 From: clintonium-119 Date: Thu, 15 Aug 2024 14:40:19 -0400 Subject: [PATCH 2/3] Update test --- .../prescriptionFormConstants.test.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/unit/pages/prescription/prescriptionFormConstants.test.js b/test/unit/pages/prescription/prescriptionFormConstants.test.js index 8ba3e1b225..14b76f6b6f 100644 --- a/test/unit/pages/prescription/prescriptionFormConstants.test.js +++ b/test/unit/pages/prescription/prescriptionFormConstants.test.js @@ -15,7 +15,7 @@ const devices = { pumps: [{ id: prescriptionFormConstants.deviceIdMap.palmtree }], }; -describe.only('prescriptionFormConstants', function() { +describe('prescriptionFormConstants', function() { it('should export the `dateFormat`', function() { expect(prescriptionFormConstants.dateFormat).to.equal('YYYY-MM-DD'); }); @@ -64,14 +64,19 @@ describe.only('prescriptionFormConstants', function() { expect(prescriptionFormConstants.validDeviceIds.pumps).to.be.an('array').and.contain(prescriptionFormConstants.deviceIdMap.palmtree); }); - it('should export a JSX element for extra info about each device', () => { + it('should export extra details about each device', () => { expect(prescriptionFormConstants.deviceDetails).to.be.an('object').and.have.keys([ prescriptionFormConstants.deviceIdMap.dexcomG6, prescriptionFormConstants.deviceIdMap.palmtree, ]); - _.each(prescriptionFormConstants.deviceDetails, info => { - expect(info.description).to.be.an('object'); - expect(info.description.props).to.be.an('object').and.have.keys(['children']); + + _.each(prescriptionFormConstants.deviceDetails, (details, deviceId) => { + expect(details.description).to.be.an('object'); + expect(details.description.props).to.be.an('object').and.have.keys(['children']); + + if (deviceId === prescriptionFormConstants.deviceIdMap.palmtree) { + expect(details.skipCalculator).to.be.true; + } }); }); From a9e59290508e2059fafeaa831bd041dfc9cdcc7b Mon Sep 17 00:00:00 2001 From: clintonium-119 Date: Thu, 15 Aug 2024 15:36:47 -0400 Subject: [PATCH 3/3] v1.81.0-web-3020-disable-ace-calculator-palmtree.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b359569fc..52fb373369 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "node": "20.8.0" }, "packageManager": "yarn@3.6.4", - "version": "1.81.0-web-3021-show-rx-code.1", + "version": "1.81.0-web-3020-disable-ace-calculator-palmtree.1", "private": true, "scripts": { "test": "TZ=UTC NODE_ENV=test NODE_OPTIONS='--max-old-space-size=4096' yarn karma start",