diff --git a/.gitignore b/.gitignore
index f422994295..3eed11d703 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,5 @@ locales/*/*_old.json
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+
+.codegpt
\ No newline at end of file
diff --git a/app/components/clinic/PatientForm.js b/app/components/clinic/PatientForm.js
index 4342731e81..2da1983c1d 100644
--- a/app/components/clinic/PatientForm.js
+++ b/app/components/clinic/PatientForm.js
@@ -20,15 +20,12 @@ import InputMask from 'react-input-mask';
import CloseRoundedIcon from '@material-ui/icons/CloseRounded';
import CheckCircleRoundedIcon from '@material-ui/icons/CheckCircleRounded';
import ErrorOutlineRoundedIcon from '@material-ui/icons/ErrorOutlineRounded';
-import { Box, Flex, Text, BoxProps } from 'theme-ui';
+import { Box, Text, BoxProps } from 'theme-ui';
import moment from 'moment';
import * as actions from '../../redux/actions';
-import Checkbox from '../../components/elements/Checkbox';
import TextInput from '../../components/elements/TextInput';
-import Button from '../../components/elements/Button';
import { TagList } from '../../components/elements/Tag';
-import ResendDexcomConnectRequestDialog from './ResendDexcomConnectRequestDialog';
import { useToasts } from '../../providers/ToastProvider';
import { getCommonFormikFieldProps } from '../../core/forms';
import { useInitialFocusedInput, useIsFirstRender, usePrevious } from '../../core/hooks';
@@ -36,31 +33,21 @@ import { dateRegex, patientSchema as validationSchema } from '../../core/clinicU
import { accountInfoFromClinicPatient } from '../../core/personutils';
import { Body0 } from '../../components/elements/FontStyles';
import { borders, colors } from '../../themes/baseTheme';
-import Icon from '../elements/Icon';
-import DexcomLogoIcon from '../../core/icons/DexcomLogo.svg';
-
-function getFormValues(source, clinicPatientTags, disableDexcom) {
- const hasDexcomDataSource = !!find(source?.dataSources, { providerName: 'dexcom' });
- const connectDexcom = (hasDexcomDataSource || (!disableDexcom && source?.connectDexcom)) || false;
- const addDexcomDataSource = connectDexcom && !hasDexcomDataSource;
+export function getFormValues(source, clinicPatientTags) {
return {
birthDate: source?.birthDate || '',
email: source?.email || '',
fullName: source?.fullName || '',
mrn: source?.mrn || '',
tags: reject(source?.tags || [], tagId => !clinicPatientTags?.[tagId]),
- connectDexcom,
- dataSources: addDexcomDataSource ? [
- ...source?.dataSources || [],
- { providerName: 'dexcom', state: 'pending' },
- ] : source?.dataSources || [],
+ dataSources: source?.dataSources || [],
};
}
-function emptyValuesFilter(value, key) {
+export function emptyValuesFilter(value, key) {
// We want to allow sending an empty `tags` array. Otherwise, strip empty fields from payload.
- return !includes(['tags', 'connectDexcom'], key) && isEmpty(value);
+ return !includes(['tags'], key) && isEmpty(value);
}
export const PatientForm = (props) => {
@@ -94,12 +81,8 @@ export const PatientForm = (props) => {
const clinicPatientTags = useMemo(() => keyBy(clinic?.patientTags, 'id'), [clinic?.patientTags]);
const dexcomDataSource = find(patient?.dataSources, { providerName: 'dexcom' });
const dexcomAuthInviteExpired = dexcomDataSource?.expirationTime < moment.utc().toISOString();
- const showConnectDexcom = action !== 'acceptInvite' && !!selectedClinicId && !dexcomDataSource;
const showEmail = action !== 'acceptInvite';
- const [disableConnectDexcom, setDisableConnectDexcom] = useState(false);
- const showDexcomConnectState = !!selectedClinicId && !!dexcomDataSource?.state;
- const [showResendDexcomConnectRequest, setShowResendDexcomConnectRequest] = useState(false);
- const { sendingPatientDexcomConnectRequest, fetchingPatientsForClinic } = useSelector((state) => state.blip.working);
+ const { fetchingPatientsForClinic } = useSelector((state) => state.blip.working);
const [patientFetchOptions, setPatientFetchOptions] = useState({});
const loggedInUserId = useSelector((state) => state.blip.loggedInUserId);
const previousFetchingPatientsForClinic = usePrevious(fetchingPatientsForClinic);
@@ -156,6 +139,7 @@ export const PatientForm = (props) => {
const formikContext = useFormik({
initialValues: getFormValues(patient, clinicPatientTags),
+ initialStatus: { showDataConnectionsModalNext: false },
onSubmit: (values, formikHelpers) => {
const context = selectedClinicId ? 'clinic' : 'vca';
@@ -163,7 +147,7 @@ export const PatientForm = (props) => {
edit: {
clinic: {
handler: 'updateClinicPatient',
- args: () => [selectedClinicId, patient.id, omitBy({ ...patient, ...getFormValues(values, clinicPatientTags, disableConnectDexcom) }, emptyValuesFilter)],
+ args: () => [selectedClinicId, patient.id, omitBy({ ...patient, ...getFormValues(values, clinicPatientTags) }, emptyValuesFilter)],
},
vca: {
handler: 'updatePatient',
@@ -173,7 +157,7 @@ export const PatientForm = (props) => {
create: {
clinic: {
handler: 'createClinicCustodialAccount',
- args: () => [selectedClinicId, omitBy(getFormValues(values, clinicPatientTags, disableConnectDexcom), emptyValuesFilter)],
+ args: () => [selectedClinicId, omitBy(getFormValues(values, clinicPatientTags), emptyValuesFilter)],
},
vca: {
handler: 'createVCACustodialAccount',
@@ -184,7 +168,7 @@ export const PatientForm = (props) => {
clinic: {
handler: 'acceptPatientInvitation',
args: () => [selectedClinicId, invite.key, invite.creatorId, omitBy(
- pick(getFormValues(values, clinicPatientTags, disableConnectDexcom), ['mrn', 'birthDate', 'fullName', 'tags']),
+ pick(getFormValues(values, clinicPatientTags), ['mrn', 'birthDate', 'fullName', 'tags']),
emptyValuesFilter
)],
},
@@ -195,57 +179,24 @@ export const PatientForm = (props) => {
trackMetric(`${selectedClinicId ? 'Clinic' : 'Clinician'} - add patient email saved`);
}
- const emailUpdated = initialValues.email && values.email && (initialValues.email !== values.email);
-
- if (context === 'clinic' && values.connectDexcom && (!patient?.lastRequestedDexcomConnectTime || emailUpdated)) {
- const reason = emailUpdated ? 'email updated' : 'initial connection request';
+ const handlerArgs = actionMap[action][context].args();
- trackMetric('Clinic - Request dexcom connection for patient', {
- clinicId: selectedClinicId,
- reason,
- action,
- });
-
- formikHelpers.setStatus('sendingDexcomConnectRequest');
+ if (context === 'clinic' && action === 'create' && clinic?.country === 'US') {
+ formikHelpers.setStatus({ showDataConnectionsModalNext: true, newPatient: handlerArgs[1] });
}
- dispatch(actions.async[actionMap[action][context].handler](api, ...actionMap[action][context].args()));
+ dispatch(actions.async[actionMap[action][context].handler](api, ...handlerArgs));
},
validationSchema: validationSchema({mrnSettings, existingMRNs}),
});
const {
- errors,
setFieldValue,
setValues,
status,
values,
} = formikContext;
- function handleAsyncResult(workingState, successMessage) {
- const { inProgress, completed, notification } = workingState;
-
- 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,
- variant: 'success',
- });
- }
-
- if (completed === false) {
- setToast({
- message: get(notification, 'message'),
- variant: 'danger',
- });
- }
- }
- }
-
// Fetchers
useEffect(() => {
if (
@@ -298,51 +249,15 @@ export const PatientForm = (props) => {
onFormChange(formikContext);
}, [values, clinicPatientTags, status]);
- useEffect(() => {
- if (includes(['create', 'edit'], action)) {
- const hasValidEmail = !isEmpty(values.email) && !errors.email;
- setDisableConnectDexcom(!hasValidEmail);
-
- if (values.connectDexcom && !hasValidEmail) {
- setFieldValue('connectDexcom', false);
- }
- }
- }, [values.email, errors.email, action]);
-
// Pull the patient on load to ensure the most recent dexcom connection state is made available
useEffect(() => {
if ((action === 'edit') && selectedClinicId && patient?.id) fetchPatientDetails();
}, []);
- useEffect(() => {
- handleAsyncResult(sendingPatientDexcomConnectRequest, t('Dexcom connection request to {{email}} has been resent.', {
- email: patient?.email,
- }));
- }, [sendingPatientDexcomConnectRequest]);
-
- function handleResendDexcomConnectEmail() {
- trackMetric('Clinic - Resend Dexcom connect email', { clinicId: selectedClinicId, dexcomConnectState, source: 'patientForm' })
- setShowResendDexcomConnectRequest(true);
- }
-
- function handleResendDexcomConnectEmailConfirm() {
- trackMetric('Clinic - Resend Dexcom connect email confirm', { clinicId: selectedClinicId, source: 'patientForm' });
- formikContext.setStatus('resendingDexcomConnectRequest');
- dispatch(actions.async.sendPatientDexcomConnectRequest(api, selectedClinicId, patient.id));
- }
-
function fetchPatientDetails() {
dispatch(actions.async.fetchPatientFromClinic(api, selectedClinicId, patient.id));
}
- function renderRegionalNote() {
- return (
-
- {t('For US Dexcom Users Only')}
-
- );
- }
-
const debounceSearch = useCallback(
debounce((search) => {
setPatientFetchOptions({
@@ -486,165 +401,6 @@ export const PatientForm = (props) => {
)}
)}
-
- {showConnectDexcom && (
-
-
-
- {t('Connect with')}
-
-
-
-
- )}
- />
-
-
- {t('If this box is checked, patient will receive an email to authorize sharing Dexcom data with Tidepool.')}
-
-
- {renderRegionalNote()}
-
- )}
-
- {showDexcomConnectState && (
-
-
-
-
-
- {dexcomConnectStateUI[dexcomConnectState].label}
-
-
-
-
-
- {dexcomConnectState === 'pending' && (
-
- {t('Patient has received an email to authorize Dexcom data sharing with Tidepool but they have not taken any action yet.')}
-
-
-
- )}
-
- {dexcomConnectState === 'pendingReconnect' && (
-
- {t('Patient has received an email to reconnect their Dexcom data with Tidepool but they have not taken any action yet.')}
-
-
-
- )}
-
- {dexcomConnectState === 'pendingExpired' && (
-
- {t('Patient invitation to authorize Dexcom data sharing with Tidepool has expired. Would you like to send a new connection request?')}
-
-
-
- )}
-
- {dexcomConnectState === 'disconnected' && (
-
- {t('Patient has disconnected their Dexcom data sharing authorization with Tidepool. Would you like to send a new connection request?')}
-
-
-
- )}
-
- {dexcomConnectState === 'error' && (
-
- {t('Patient\'s previous Dexcom authorization is no longer valid. Would you like to send a new connection request?')}
-
-
-
- )}
-
- {dexcomConnectStateUI[dexcomConnectState].showRegionalNote && renderRegionalNote()}
-
- setShowResendDexcomConnectRequest(false)}
- onConfirm={handleResendDexcomConnectEmailConfirm}
- open={showResendDexcomConnectRequest}
- patient={patient}
- t={t}
- trackMetric={trackMetric}
- />
-
- )}
-
);
};
diff --git a/app/components/clinic/PatientLastReviewed.js b/app/components/clinic/PatientLastReviewed.js
index efaf0f1f20..9da1d1c907 100644
--- a/app/components/clinic/PatientLastReviewed.js
+++ b/app/components/clinic/PatientLastReviewed.js
@@ -7,6 +7,7 @@ import moment from 'moment-timezone';
import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
import { utils as vizUtils } from '@tidepool/viz';
import get from 'lodash/get';
+import upperFirst from 'lodash/upperFirst';
import HoverButton from '../elements/HoverButton';
import Icon from '../elements/Icon';
@@ -79,7 +80,7 @@ export const PatientLastReviewed = ({ api, patientId, recentlyReviewedThresholdD
let clickHandler = handleReview;
let buttonText = t('Mark Reviewed');
- let formattedLastReviewed = { text: '-' };
+ let formattedLastReviewed = { daysText: '-' };
let lastReviewIsToday = false;
let reviewIsRecent = false;
let canReview = true;
@@ -132,7 +133,7 @@ export const PatientLastReviewed = ({ api, patientId, recentlyReviewedThresholdD
}}
>
{reviewIsRecent && }
- {formattedLastReviewed.text}
+ {upperFirst(formattedLastReviewed.daysText)}
diff --git a/app/components/clinic/ResendDexcomConnectRequestDialog.js b/app/components/clinic/ResendDataSourceConnectRequestDialog.js
similarity index 57%
rename from app/components/clinic/ResendDexcomConnectRequestDialog.js
rename to app/components/clinic/ResendDataSourceConnectRequestDialog.js
index 9be5e0dc82..16347e7cea 100644
--- a/app/components/clinic/ResendDexcomConnectRequestDialog.js
+++ b/app/components/clinic/ResendDataSourceConnectRequestDialog.js
@@ -5,25 +5,27 @@ import { useSelector } from 'react-redux';
import { Text } from 'theme-ui';
import sundial from 'sundial';
-import Button from '../../components/elements/Button';
-import { Body1, MediumTitle } from '../../components/elements/FontStyles';
+import Button from '../elements/Button';
+import { Body1, MediumTitle } from '../elements/FontStyles';
+import { providers } from '../datasources/DataConnections';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
-} from '../../components/elements/Dialog';
+} from '../elements/Dialog';
-export const ResendDexcomConnectRequestDialog = (props) => {
- const { t, onClose, onConfirm, open, patient } = props;
+export const ResendDataSourceConnectRequestDialog = (props) => {
+ const { t, onClose, onConfirm, open, patient, providerName } = props;
const timePrefs = useSelector((state) => state.blip.timePrefs);
- const { sendingPatientDexcomConnectRequest } = useSelector((state) => state.blip.working);
+ const { sendingPatientDataProviderConnectRequest } = useSelector((state) => state.blip.working);
+ const providerDisplayName = providers[providerName]?.displayName;
- const formattedLastRequestedDexcomConnectDate =
- patient?.lastRequestedDexcomConnectTime &&
+ const formattedLastRequestedDataSourceConnectDate =
+ patient?.connectionRequests?.[providerName]?.[0]?.createdTime &&
sundial.formatInTimezone(
- patient?.lastRequestedDexcomConnectTime,
+ patient.connectionRequests[providerName][0].createdTime,
timePrefs?.timezoneName ||
new Intl.DateTimeFormat().resolvedOptions().timeZone,
'MM/DD/YYYY [at] h:mm a'
@@ -31,7 +33,7 @@ export const ResendDexcomConnectRequestDialog = (props) => {
return (
);
}, [
+ handleCloseOverlays,
showRpmReportLimitDialog,
t,
]);
- function handleCloseOverlays() {
- const resetList = showAddPatientDialog || showEditPatientDialog;
- setShowDeleteDialog(false);
- setShowAddPatientDialog(false);
- setShowEditPatientDialog(false);
- setShowClinicPatientTagsDialog(false);
- setShowTimeInRangeDialog(false);
- setShowSendUploadReminderDialog(false);
- setShowTideDashboardConfigDialog(false);
- setShowRpmReportConfigDialog(false);
- setShowRpmReportLimitDialog(false);
-
- if (resetList) {
- setPatientFetchOptions({ ...patientFetchOptions });
- }
-
- setTimeout(() => {
- setSelectedPatient(null);
- });
- }
+ const renderDataConnectionsModal = useCallback(() => {
+ return (
+ {
+ setShowDataConnectionsModal(false)
+ setShowEditPatientDialog(true)
+ } : undefined}
+ />
+ );
+ }, [
+ handleCloseOverlays,
+ patientFormContext?.status,
+ selectedPatient,
+ ]);
const renderPatient = useCallback(patient => (
@@ -2794,7 +2876,7 @@ export const ClinicPatients = (props) => {
whiteSpace: 'nowrap',
}}
>
- {formattedLastDataDateCGM.text}
+ {upperFirst(formattedLastDataDateCGM.daysText)}
)}
@@ -2809,7 +2891,7 @@ export const ClinicPatients = (props) => {
whiteSpace: 'nowrap',
}}
>
- {formattedLastDataDateBGM.text}
+ {upperFirst(formattedLastDataDateBGM.daysText)}
)}
@@ -2978,6 +3060,7 @@ export const ClinicPatients = (props) => {
t={t}
trackMetric={trackMetric}
setSelectedPatient={setSelectedPatient}
+ setShowDataConnectionsModal={setShowDataConnectionsModal}
setShowEditPatientDialog={setShowEditPatientDialog}
prefixPopHealthMetric={prefixPopHealthMetric}
setShowSendUploadReminderDialog={setShowSendUploadReminderDialog}
@@ -3250,6 +3333,8 @@ export const ClinicPatients = (props) => {
{showTimeInRangeDialog && renderTimeInRangeDialog()}
{showSendUploadReminderDialog && renderSendUploadReminderDialog()}
{showClinicPatientTagsDialog && renderClinicPatientTagsDialog()}
+ {showDataConnectionsModal && renderDataConnectionsModal()}
+
{
+ let start = startDate;
+ let end = endDate;
+
+ if (isNumber(startDate) && isNumber(endDate)) {
+ start = startDate - getOffset(startDate, timezone) * MS_IN_MIN;
+ end = endDate - getOffset(endDate, timezone) * MS_IN_MIN;
+ }
+
+ return formatDateRange(start, end, dateParseFormat, monthFormat);
+};
+
+const getReportDaysText = (newestDatum, oldestDatum, bgDaysWorn, timezone) => {
+ const reportDaysText = bgDaysWorn === 1
+ ? moment.utc(newestDatum?.time - getOffset(newestDatum?.time, timezone) * MS_IN_MIN).format('MMMM D, YYYY')
+ : getDateRange(oldestDatum?.time, newestDatum?.time, undefined, '', 'MMMM', timezone);
+
+ return reportDaysText;
+};
+
+export default getReportDaysText;
\ No newline at end of file
diff --git a/app/pages/dashboard/PatientDrawer/CGMStatistics.js b/app/pages/dashboard/PatientDrawer/CGMStatistics/index.js
similarity index 58%
rename from app/pages/dashboard/PatientDrawer/CGMStatistics.js
rename to app/pages/dashboard/PatientDrawer/CGMStatistics/index.js
index 1e8ebfa8c0..e86c8e526e 100644
--- a/app/pages/dashboard/PatientDrawer/CGMStatistics.js
+++ b/app/pages/dashboard/PatientDrawer/CGMStatistics/index.js
@@ -1,25 +1,12 @@
import React from 'react';
-import colorPalette from '../../../themes/colorPalette';
+import colorPalette from '../../../../themes/colorPalette';
import { useTranslation } from 'react-i18next';
-import { useSelector } from 'react-redux';
import { Flex, Box, Text } from 'theme-ui';
-import moment from 'moment';
import { utils as vizUtils } from '@tidepool/viz';
-import utils from '../../../core/utils';
-import { MGDL_UNITS } from '../../../core/constants';
-
-const formatDateRange = (startEndpoint, endEndpoint, timezoneName) => {
- const startDate = moment.utc(startEndpoint).tz(timezoneName);
- const endDate = moment.utc(endEndpoint).tz(timezoneName);
- const startYear = startDate.year();
- const endYear = endDate.year();
-
- if (startYear !== endYear) {
- return `${startDate.format('MMMM D, YYYY')} - ${endDate.format('MMMM D, YYYY')}`;
- }
-
- return `${startDate.format('MMMM D')} - ${endDate.format('MMMM D')}, ${endDate.format('YYYY')}`;
-}
+const { formatDatum, bankersRound } = vizUtils.stat;
+const { getTimezoneFromTimePrefs } = vizUtils.datetime;
+import { MGDL_UNITS } from '../../../../core/constants';
+import getReportDaysText from './getReportDaysText';
const TableRow = ({ label, sublabel, value, units, id }) => {
return (
@@ -46,8 +33,8 @@ const TableRow = ({ label, sublabel, value, units, id }) => {
{units && {units}}
- )
-}
+ );
+};
const CGMStatistics = ({ agpCGM }) => {
const { t } = useTranslation();
@@ -56,32 +43,31 @@ const CGMStatistics = ({ agpCGM }) => {
const {
timePrefs,
- bgPrefs: { bgUnits },
+ bgPrefs,
data: {
current: {
- endpoints: { days: endpointDays },
stats: {
- bgExtents: { newestDatum, oldestDatum },
+ bgExtents: { newestDatum, oldestDatum, bgDaysWorn },
sensorUsage: { sensorUsageAGP },
averageGlucose: { averageGlucose },
glucoseManagementIndicator: { glucoseManagementIndicatorAGP },
- coefficientOfVariation: { coefficientOfVariation }
+ coefficientOfVariation: { coefficientOfVariation },
},
- }
- }
+ },
+ },
} = agpCGM;
- const timezoneName = vizUtils.datetime.getTimezoneFromTimePrefs(timePrefs);
+ const { bgUnits } = bgPrefs;
- const avgGlucosePrecision = bgUnits === MGDL_UNITS ? 0 : 1;
- const avgGlucoseTarget = bgUnits === MGDL_UNITS ? '154' : '8.6';
+ const timezone = getTimezoneFromTimePrefs(timePrefs);
- const dateRange = formatDateRange(oldestDatum.time, newestDatum.time, timezoneName);
- const daySpan = endpointDays;
- const cgmActive = utils.roundToPrecision(sensorUsageAGP, 1);
- const avgGlucose = utils.roundToPrecision(averageGlucose, avgGlucosePrecision);
- const gmi = utils.roundToPrecision(glucoseManagementIndicatorAGP, 1);
- const cov = utils.roundToPrecision(coefficientOfVariation, 1);
+ const avgGlucoseTarget = bgUnits === MGDL_UNITS ? '154' : '8.6';
+
+ const dateRange = getReportDaysText(newestDatum, oldestDatum, bgDaysWorn, timezone);
+ const cgmActive = bankersRound(sensorUsageAGP, 1);
+ const avgGlucose = formatDatum({ value: averageGlucose }, 'bgValue', { bgPrefs, useAGPFormat: true });
+ const gmi = formatDatum({ value: glucoseManagementIndicatorAGP }, 'gmi', { bgPrefs, useAGPFormat: true });
+ const cov = formatDatum({ value: coefficientOfVariation }, 'cv', { bgPrefs, useAGPFormat: true });
return (
@@ -89,7 +75,7 @@ const CGMStatistics = ({ agpCGM }) => {
{
id="agp-table-avg-glucose"
label={t('Average Glucose')}
sublabel={t('(Goal <{{avgGlucoseTarget}} {{bgUnits}})', { avgGlucoseTarget, bgUnits })}
- value={`${avgGlucose}`}
+ value={avgGlucose?.value}
units={` ${bgUnits}`}
/>
- )
-}
+ );
+};
export default CGMStatistics;
\ No newline at end of file
diff --git a/app/pages/dashboard/PatientDrawer/Content.js b/app/pages/dashboard/PatientDrawer/Content.js
index b117e44067..686eea9f62 100644
--- a/app/pages/dashboard/PatientDrawer/Content.js
+++ b/app/pages/dashboard/PatientDrawer/Content.js
@@ -61,7 +61,8 @@ const Content = ({ api, patientId, agpPeriodInDays }) => {
if (status === STATUS.NO_PATIENT_DATA) return ;
if (status === STATUS.INSUFFICIENT_DATA) return ;
- if (status !== STATUS.SVGS_GENERATED) return
+
+ if (status !== STATUS.SVGS_GENERATED) return ;
const percentInRanges = svgDataURLS?.agpCGM?.percentInRanges;
const ambulatoryGlucoseProfile = svgDataURLS?.agpCGM?.ambulatoryGlucoseProfile;
diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js b/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js
index 549b454655..9087ea7863 100644
--- a/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js
+++ b/app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.js
@@ -1,119 +1,50 @@
import React, { useEffect, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Button from '../../../../components/elements/Button';
-import utils from '../../../../core/utils';
-import { MGDL_UNITS } from '../../../../core/constants';
+import { MS_IN_HOUR } from '../../../../core/constants';
+import { Box } from 'theme-ui';
import { utils as vizUtils } from '@tidepool/viz';
-const { TextUtil } = vizUtils.text;
-import { Box } from 'theme-ui'
-import moment from 'moment';
-
-const formatDateRange = (startEndpoint, endEndpoint, timezoneName) => {
- const startDate = moment.utc(startEndpoint).tz(timezoneName);
- const endDate = moment.utc(endEndpoint).tz(timezoneName);
- const startYear = startDate.year();
- const endYear = endDate.year();
-
- if (startYear !== endYear) {
- return `${startDate.format('MMMM D, YYYY')} - ${endDate.format('MMMM D, YYYY')}`;
- }
-
- return `${startDate.format('MMMM D')} - ${endDate.format('MMMM D')}, ${endDate.format('YYYY')}`;
-}
-
-const getCGMClipboardText = (patient, agpCGM, t) => {
- if (!agpCGM || !patient) return '';
-
- const { fullName, birthDate } = patient;
-
- const {
- timePrefs,
- bgPrefs: { bgUnits },
- data: {
- current: {
- stats: {
- bgExtents: { newestDatum, oldestDatum },
- averageGlucose: { averageGlucose },
- timeInRange: { counts },
- },
- }
- }
- } = agpCGM;
-
- const timezoneName = vizUtils.datetime.getTimezoneFromTimePrefs(timePrefs);
-
- const currentDate = moment().format('MMMM Do, YYYY');
-
- // TODO: Add test for no data scenario
- const dateRange = formatDateRange(oldestDatum?.time, newestDatum?.time, timezoneName);
-
- const targetRange = bgUnits === MGDL_UNITS ? '70-180' : '3.9-10.0';
- const lowRange = bgUnits === MGDL_UNITS ? '54-70' : '3.0-3.9';
- const veryLowRange = bgUnits === MGDL_UNITS ? '<54' : '<3.0';
-
- const countsInTarget = utils.roundToPrecision((counts.target / counts.total) * 100, 0);
- const countsInLow = utils.roundToPrecision((counts.low * 100 ) / counts.total, 0);
- const countsInVeryLow = utils.roundToPrecision((counts.veryLow * 100 ) / counts.total, 0);
-
- const avgGlucose = utils.roundToPrecision(averageGlucose, bgUnits === MGDL_UNITS ? 0 : 1);
-
- const textUtil = new TextUtil();
- let clipboardText = '';
-
- clipboardText += textUtil.buildTextLine(fullName);
- clipboardText += textUtil.buildTextLine(t('Date of birth: {{birthDate}}', { birthDate }));
- clipboardText += textUtil.buildTextLine(t('Exported from Tidepool TIDE: {{currentDate}}', { currentDate }));
- clipboardText += textUtil.buildTextLine('');
- clipboardText += textUtil.buildTextLine(t('Reporting Period: {{dateRange}}', { dateRange }));
- clipboardText += textUtil.buildTextLine('');
- clipboardText += textUtil.buildTextLine(t('Avg. Daily Time In Range ({{bgUnits}})', { bgUnits }));
- clipboardText += textUtil.buildTextLine(t('{{targetRange}} {{countsInTarget}}%', { targetRange, countsInTarget }));
- clipboardText += textUtil.buildTextLine(t('{{lowRange}} {{countsInLow}}%', { lowRange, countsInLow }));
- clipboardText += textUtil.buildTextLine(t('{{veryLowRange}} {{countsInVeryLow}}%', { veryLowRange, countsInVeryLow }));
- clipboardText += textUtil.buildTextLine('');
- clipboardText += textUtil.buildTextLine(t('Avg. Glucose (CGM): {{avgGlucose}} {{bgUnits}}', { avgGlucose, bgUnits }));
-
- return clipboardText;
-}
+const { agpCGMText } = vizUtils.text;
const STATE = {
DEFAULT: 'DEFAULT',
CLICKED: 'CLICKED',
-}
+};
-const CGMClipboardButton = ({ patient, agpCGM }) => {
+const CGMClipboardButton = ({ patient, data }) => {
const { t } = useTranslation();
const [buttonState, setButtonState] = useState(STATE.DEFAULT);
+ const clipboardText = useMemo(() => agpCGMText(patient, data), [patient, data]);
useEffect(() => {
let buttonTextEffect = setTimeout(() => {
- setButtonState(STATE.DEFAULT)
+ setButtonState(STATE.DEFAULT);
}, 1000);
return () => {
clearTimeout(buttonTextEffect);
- }
- }, [buttonState])
+ };
+ }, [buttonState]);
- const sensorUsage = agpCGM?.data?.current?.stats?.sensorUsage?.sensorUsage || 0;
+ const { count, sampleFrequency } = data?.data?.current?.stats?.sensorUsage || {};
- const isDisabled = !agpCGM || sensorUsage < 86400000; // minimum 24 hours
+ const hoursOfCGMData = (count * sampleFrequency) / MS_IN_HOUR;
- const clipboardText = useMemo(() => getCGMClipboardText(patient, agpCGM, t), [patient, agpCGM, t]);
+ const isDataInsufficient = !hoursOfCGMData || hoursOfCGMData < 24;
const handleCopy = () => {
navigator?.clipboard?.writeText(clipboardText);
setButtonState(STATE.CLICKED);
- }
+ };
return (
-
+
{buttonState === STATE.CLICKED
? {t('Copied ✓')}
: {t('Copy as Text')}
}
- )
-}
+ );
+};
export default CGMClipboardButton;
\ No newline at end of file
diff --git a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js b/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js
index e001a1b62b..05a5883836 100644
--- a/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js
+++ b/app/pages/dashboard/PatientDrawer/MenuBar/MenuBar.js
@@ -19,7 +19,7 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
const selectedClinicId = useSelector(state => state.blip.selectedClinicId);
const patient = useSelector(state => state.blip.clinics[state.blip.selectedClinicId]?.patients?.[patientId]);
- const agpCGM = useSelector(state => state.blip.pdf?.data?.agpCGM); // IMPORTANT: Data taken from Redux PDF slice
+ 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
@@ -35,10 +35,10 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
const handleReviewSuccess = () => {
setTimeout(() => {
onClose();
- }, 500)
- }
+ }, 500);
+ };
- const { fullName, birthDate } = patient || {};
+ const { fullName, birthDate } = patient || {};
return (
@@ -47,7 +47,7 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
{fullName}
{ birthDate &&
-
+
{t('DOB: {{birthDate}}', { birthDate })}
}
@@ -60,7 +60,7 @@ const MenuBar = ({ patientId, api, trackMetric, onClose }) => {
-
+
diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js b/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js
index 13b543dca3..34867d76d6 100644
--- a/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js
+++ b/app/pages/dashboard/PatientDrawer/useAgpCGM/getOpts.js
@@ -2,6 +2,7 @@ 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';
const getTimezoneFromTimePrefs = vizUtils.datetime.getTimezoneFromTimePrefs;
@@ -9,37 +10,28 @@ const getOpts = (
data, // data from redux (state.blip.data)
agpPeriodInDays,
) => {
- const getMostRecentDatumTimeByChartType = (data, chartType) => {
- let latestDatums;
+ const getMostRecentDatumTimeByChartType = (data, _chartType) => {
const getLatestDatums = types => _.pick(_.get(data, 'metaData.latestDatumByType'), types);
- switch (chartType) { // cases for 'trends', 'bgLog', 'daily', and 'basics' omitted
- case 'agpBGM':
- latestDatums = getLatestDatums([
- 'smbg',
- ]);
- break;
-
- case 'agpCGM':
- latestDatums = getLatestDatums([
- 'cbg',
- ]);
- break;
-
- default:
- latestDatums = [];
- break;
- }
-
+ let latestDatums = getLatestDatums(['cbg']) || [];
+
return _.max(_.map(latestDatums, d => (d.normalEnd || d.normalTime)));
- }
+ };
const mostRecentDatumDates = {
- agpBGM: getMostRecentDatumTimeByChartType(data, 'agpBGM'),
agpCGM: getMostRecentDatumTimeByChartType(data, 'agpCGM'),
};
- const timezoneName = getTimezoneFromTimePrefs(data?.timePrefs);
+ const timePrefs = (() => {
+ const latestTimeZone = data?.metaData?.latestTimeZone;
+ const queryParams = {};
+
+ const localTimePrefs = utils.getTimePrefsForDataProcessing(latestTimeZone, queryParams);
+
+ return localTimePrefs;
+ })();
+
+ const timezoneName = getTimezoneFromTimePrefs(timePrefs);
const endOfToday = moment.utc().tz(timezoneName).endOf('day').subtract(1, 'ms');
@@ -59,14 +51,7 @@ const getOpts = (
});
};
- const defaultDates = () => ({
- agpBGM: getLastNDays(agpPeriodInDays, 'agpBGM'),
- agpCGM: getLastNDays(agpPeriodInDays, 'agpCGM'),
-
- // 'trends', 'bgLog', 'daily', and 'basics' omitted
- });
-
- const dates = defaultDates();
+ const dates = getLastNDays(agpPeriodInDays, 'agpCGM');
const formatDateEndpoints = ({ startDate, endDate }) => (startDate && endDate ? [
startDate.valueOf(),
@@ -74,9 +59,8 @@ const getOpts = (
] : []);
const opts = {
- agpCGM: { disabled: false, endpoints: formatDateEndpoints(dates.agpCGM) },
- agpBGM: { disabled: false, endpoints: formatDateEndpoints(dates.agpBGM) },
-
+ agpCGM: { disabled: false, endpoints: formatDateEndpoints(dates) },
+ agpBGM: { disabled: true },
basics: { disabled: true },
bgLog: { disabled: true },
daily: { disabled: true },
diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js b/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js
index b3bc9222f7..ef04b9ee60 100644
--- a/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js
+++ b/app/pages/dashboard/PatientDrawer/useAgpCGM/getQueries.js
@@ -1,19 +1,8 @@
import _ from 'lodash';
import { utils as vizUtils } from '@tidepool/viz';
-import utils from '../../../../core/utils';
+const { commonStats } = vizUtils.stat;
-const chartPrefs = {
- agpBGM: {
- bgSource: 'smbg',
- },
- agpCGM: {
- bgSource: 'cbg',
- },
- settings: {
- touched: false,
- },
- excludedDevices: [],
-};
+import utils from '../../../../core/utils';
const getQueries = (
data,
@@ -44,65 +33,30 @@ const getQueries = (
return localTimePrefs;
})();
- const getStatsByChartType = (chartType, _bgSource) => {
- const { commonStats } = vizUtils.stat;
-
- let stats = [];
-
- switch (chartType) { // cases 'basics', 'trends', 'bgLog', and 'daily' omitted
- case 'agpBGM':
- stats.push(commonStats.averageGlucose,);
- stats.push(commonStats.bgExtents,);
- stats.push(commonStats.coefficientOfVariation,);
- stats.push(commonStats.glucoseManagementIndicator,);
- stats.push(commonStats.readingsInRange,);
- break;
-
- case 'agpCGM':
- stats.push(commonStats.averageGlucose);
- stats.push(commonStats.bgExtents);
- stats.push(commonStats.coefficientOfVariation);
- stats.push(commonStats.glucoseManagementIndicator);
- stats.push(commonStats.sensorUsage);
- stats.push(commonStats.timeInRange);
- break;
- }
-
- return stats;
- }
-
- const commonQueries = {
- bgPrefs: bgPrefs,
- metaData: 'latestPumpUpload, bgSources',
- timePrefs: timePrefs,
- excludedDevices: chartPrefs?.excludedDevices,
- };
-
- const queries = {}
-
- if (!opts.agpBGM?.disabled) {
- queries.agpBGM = {
- endpoints: opts.agpBGM?.endpoints,
- aggregationsByDate: 'dataByDate, statsByDate',
- bgSource: _.get(chartPrefs, 'agpBGM.bgSource'),
- stats: getStatsByChartType('agpBGM'),
- types: { smbg: {} },
- ...commonQueries,
- };
- }
-
- if (!opts.agpCGM?.disabled) {
- queries.agpCGM = {
+ let stats = [
+ commonStats.averageGlucose,
+ commonStats.bgExtents,
+ commonStats.coefficientOfVariation,
+ commonStats.glucoseManagementIndicator,
+ commonStats.sensorUsage,
+ commonStats.timeInRange,
+ ];
+
+ const queries = {
+ agpCGM: {
endpoints: opts.agpCGM?.endpoints,
aggregationsByDate: 'dataByDate, statsByDate',
- bgSource: _.get(chartPrefs, 'agpCGM.bgSource'),
- stats: getStatsByChartType('agpCGM'),
+ bgSource: 'cbg',
+ stats,
types: { cbg: {} },
- ...commonQueries,
- };
- }
+ bgPrefs,
+ metaData: 'latestPumpUpload, bgSources',
+ timePrefs,
+ excludedDevices: [],
+ },
+ };
return queries;
-}
+};
export default getQueries;
\ No newline at end of file
diff --git a/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js b/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js
index 9bcdc62fa6..3fc072d71e 100644
--- a/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js
+++ b/app/pages/dashboard/PatientDrawer/useAgpCGM/useAgpCGM.js
@@ -17,7 +17,7 @@ export const STATUS = {
// Other states
NO_PATIENT_DATA: 'NO_PATIENT_DATA',
INSUFFICIENT_DATA: 'INSUFFICIENT_DATA',
-}
+};
// TODO: Revisit best way to listen for progress when we move away from blip.working
const inferLastCompletedStep = (patientId, data, pdf) => {
@@ -33,9 +33,11 @@ const inferLastCompletedStep = (patientId, data, pdf) => {
// Insufficient Data States ---
const hasNoPatientData = data.metaData?.size === 0;
const hasInsufficientData = !!pdf?.opts?.svgDataURLS && !pdf?.opts?.svgDataURLS.agpCGM;
+ const hasNoCGMData = !!pdf?.combined && !pdf?.data;
if (hasNoPatientData) return STATUS.NO_PATIENT_DATA;
if (hasInsufficientData) return STATUS.INSUFFICIENT_DATA;
+ if (hasNoCGMData) return STATUS.INSUFFICIENT_DATA;
// Happy Path States ---
const hasImagesInState = !!pdf?.opts?.svgDataURLS;
@@ -47,7 +49,7 @@ const inferLastCompletedStep = (patientId, data, pdf) => {
if (hasPatientInState) return STATUS.PATIENT_LOADED;
return STATUS.STATE_CLEARED;
-}
+};
const FETCH_PATIENT_OPTS = { forceDataWorkerAddDataRequest: true, useCache: false };
@@ -104,7 +106,7 @@ const useAgpCGM = (
return () => {
dispatch(actions.worker.removeGeneratedPDFS());
dispatch(actions.worker.dataWorkerRemoveDataRequest(null, patientId));
- }
+ };
}, []);
// Note: probably unnecessary; failsafe to ensure that data is being returned for correct patient
@@ -115,6 +117,6 @@ const useAgpCGM = (
svgDataURLS: isCorrectPatientInState ? pdf.opts?.svgDataURLS : null,
agpCGM: isCorrectPatientInState ? pdf.data?.agpCGM : null,
};
-}
+};
export default useAgpCGM;
\ No newline at end of file
diff --git a/app/pages/dashboard/TideDashboard.js b/app/pages/dashboard/TideDashboard.js
index 14bc8b12a2..f85a96502b 100644
--- a/app/pages/dashboard/TideDashboard.js
+++ b/app/pages/dashboard/TideDashboard.js
@@ -49,6 +49,7 @@ import { TagList } from '../../components/elements/Tag';
import PatientForm from '../../components/clinic/PatientForm';
import TideDashboardConfigForm, { validateTideConfig } from '../../components/clinic/TideDashboardConfigForm';
import BgSummaryCell from '../../components/clinic/BgSummaryCell';
+import DataConnectionsModal from '../../components/datasources/DataConnectionsModal';
import Popover from '../../components/elements/Popover';
import PopoverMenu from '../../components/elements/PopoverMenu';
import RadioGroup from '../../components/elements/RadioGroup';
@@ -77,6 +78,7 @@ import {
} from '../../core/clinicUtils';
import { DEFAULT_FILTER_THRESHOLDS, MGDL_UNITS, MMOLL_UNITS } from '../../core/constants';
+import DataInIcon from '../../core/icons/DataInIcon.svg';
import { colors, fontWeights, radii } from '../../themes/baseTheme';
import PatientLastReviewed from '../../components/clinic/PatientLastReviewed';
@@ -106,18 +108,29 @@ const editPatient = (patient, setSelectedPatient, selectedClinicId, trackMetric,
setShowEditPatientDialog(true);
};
+const editPatientDataConnections = (patient, setSelectedPatient, selectedClinicId, trackMetric, setShowDataConnectionsModal, source) => {
+ trackMetric('Clinic - Edit patient data connections', { clinicId: selectedClinicId, source });
+ setSelectedPatient(patient);
+ setShowDataConnectionsModal(true);
+};
+
const MoreMenu = React.memo(({
patient,
selectedClinicId,
t,
trackMetric,
setSelectedPatient,
+ setShowDataConnectionsModal,
setShowEditPatientDialog,
}) => {
const handleEditPatient = useCallback(() => {
editPatient(patient, setSelectedPatient, selectedClinicId, trackMetric, setShowEditPatientDialog, 'action menu');
}, [patient, setSelectedPatient, selectedClinicId, trackMetric, setShowEditPatientDialog]);
+ const handleEditPatientDataConnections = useCallback(() => {
+ editPatientDataConnections(patient, setSelectedPatient, selectedClinicId, trackMetric, setShowDataConnectionsModal, 'action menu');
+ }, [patient, setSelectedPatient, selectedClinicId, trackMetric, setShowDataConnectionsModal]);
+
const items = useMemo(() => ([{
icon: EditIcon,
iconLabel: t('Edit Patient Information'),
@@ -129,6 +142,17 @@ const MoreMenu = React.memo(({
handleEditPatient(patient);
},
text: t('Edit Patient Information'),
+ }, {
+ iconSrc: DataInIcon,
+ iconLabel: t('Bring Data into Tidepool'),
+ iconPosition: 'left',
+ id: `edit-data-connections-${patient.id}`,
+ variant: 'actionListItem',
+ onClick: (_popupState) => {
+ _popupState.close();
+ handleEditPatientDataConnections(patient);
+ },
+ text: t('Bring Data into Tidepool'),
}]), [
handleEditPatient,
patient,
@@ -226,8 +250,7 @@ const SortPopover = React.memo(props => {
)
-})
-
+});
const TideDashboardSection = React.memo(props => {
const {
@@ -246,6 +269,7 @@ const TideDashboardSection = React.memo(props => {
selectedClinicId,
setSections,
setSelectedPatient,
+ setShowDataConnectionsModal,
setShowEditPatientDialog,
showTideDashboardLastReviewed,
showTideDashboardPatientDrawer,
@@ -442,6 +466,7 @@ const TideDashboardSection = React.memo(props => {
t={t}
trackMetric={trackMetric}
setSelectedPatient={setSelectedPatient}
+ setShowDataConnectionsModal={setShowDataConnectionsModal}
setShowEditPatientDialog={setShowEditPatientDialog}
prefixTideDashboardMetric={prefixTideDashboardMetric}
/>;
@@ -754,6 +779,7 @@ export const TideDashboard = (props) => {
const location = useLocation();
const history = useHistory();
const [showTideDashboardConfigDialog, setShowTideDashboardConfigDialog] = useState(false);
+ const [showDataConnectionsModal, setShowDataConnectionsModal] = useState(false);
const [showEditPatientDialog, setShowEditPatientDialog] = useState(false);
const [selectedPatient, setSelectedPatient] = useState(null);
const [loading, setLoading] = useState(false);
@@ -797,6 +823,17 @@ export const TideDashboard = (props) => {
const [sections, setSections] = useState(defaultSections);
+ function handleCloseOverlays() {
+ setShowTideDashboardConfigDialog(false);
+ setShowDataConnectionsModal(false);
+ setShowEditPatientDialog(false);
+
+ setTimeout(() => {
+ setPatientFormContext(null);
+ setSelectedPatient(null);
+ });
+ }
+
const handleAsyncResult = useCallback((workingState, successMessage, onComplete = handleCloseOverlays) => {
const { inProgress, completed, notification, prevInProgress } = workingState;
@@ -820,23 +857,29 @@ export const TideDashboard = (props) => {
}
}, [isFirstRender, setToast]);
- useEffect(() => {
- handleAsyncResult({ ...updatingClinicPatient, prevInProgress: previousUpdatingClinicPatient?.inProgress }, t('You have successfully updated a patient.'), () => {
+ const handlePatientEdited = useCallback(() => {
+ if (patientFormContext?.status?.showDataConnectionsModalNext) {
+ setShowEditPatientDialog(false);
+ editPatientDataConnections(selectedPatient, setSelectedPatient, selectedClinicId, trackMetric, setShowDataConnectionsModal, 'Tide dashboard - patient modal');
+ } else {
handleCloseOverlays();
+ }
+ }, [handleCloseOverlays, patientFormContext?.status]);
- if (patientFormContext?.status === 'sendingDexcomConnectRequest') {
- dispatch(actions.async.sendPatientDexcomConnectRequest(api, selectedClinicId, updatingClinicPatient.patientId));
- }
- });
+ useEffect(() => {
+ // Only process detected updates if patient edit form is showing. Other child components, such as
+ // the PatientEmailModal, may also update the patient, and handle the results
+ if (showEditPatientDialog) {
+ handleAsyncResult({ ...updatingClinicPatient, prevInProgress: previousUpdatingClinicPatient?.inProgress }, t('You have successfully updated a patient.'), handlePatientEdited)
+ }
}, [
- api,
- dispatch,
- selectedClinicId,
handleAsyncResult,
+ handlePatientEdited,
t,
updatingClinicPatient,
patientFormContext?.status,
previousUpdatingClinicPatient?.inProgress,
+ showEditPatientDialog,
]);
// Provide latest patient state for the edit form upon fetch
@@ -930,6 +973,12 @@ export const TideDashboard = (props) => {
patientFormContext?.handleSubmit();
}, [patientFormContext, selectedClinicId, trackMetric, selectedPatient?.tags]);
+ const handleEditPatientAndAddDataSourcesConfirm = useCallback(() => {
+ trackMetric('Clinic - Edit patient next', { clinicId: selectedClinicId, source: 'Tide dashboard' });
+ patientFormContext?.setStatus({ showDataConnectionsModalNext: true });
+ handleEditPatientConfirm();
+ }, [patientFormContext, selectedClinicId, trackMetric, handleEditPatientConfirm]);
+
const handleClosePatientDrawer = useCallback(() => {
const { search, pathname } = location;
@@ -1107,17 +1156,27 @@ export const TideDashboard = (props) => {
{
- trackMetric('Clinic - Edit patient cancel', { clinicId: selectedClinicId });
- handleCloseOverlays()
+ trackMetric('Clinic - Edit patient cancel', { clinicId: selectedClinicId, source: 'TIDE dashboard' });
+ handleCloseOverlays();
}}>
{t('Cancel')}
+
+ {t('Save & Next')}
+
+
{t('Save Changes')}
@@ -1129,8 +1188,9 @@ export const TideDashboard = (props) => {
api,
existingMRNs,
handleEditPatientConfirm,
+ handleEditPatientAndAddDataSourcesConfirm,
mrnSettings,
- patientFormContext?.values,
+ patientFormContext,
selectedClinicId,
selectedPatient,
showEditPatientDialog,
@@ -1139,14 +1199,23 @@ export const TideDashboard = (props) => {
updatingClinicPatient.inProgress
]);
- function handleCloseOverlays() {
- setShowTideDashboardConfigDialog(false);
- setShowEditPatientDialog(false);
-
- setTimeout(() => {
- setSelectedPatient(null);
- });
- }
+ const renderDataConnectionsModal = useCallback(() => {
+ return (
+ {
+ setShowDataConnectionsModal(false)
+ setShowEditPatientDialog(true)
+ } : undefined}
+ />
+ );
+ }, [
+ handleCloseOverlays,
+ patientFormContext?.status,
+ selectedPatient,
+ ]);
const renderPatientGroups = useCallback(() => {
const sectionProps = {
@@ -1161,6 +1230,7 @@ export const TideDashboard = (props) => {
selectedClinicId,
setSections,
setSelectedPatient,
+ setShowDataConnectionsModal,
setShowEditPatientDialog,
showTideDashboardLastReviewed,
showTideDashboardPatientDrawer,
@@ -1246,6 +1316,7 @@ export const TideDashboard = (props) => {
{patientGroups && renderPatientGroups()}
{showTideDashboardConfigDialog && renderTideDashboardConfigDialog()}
{showEditPatientDialog && renderEditPatientDialog()}
+ {showDataConnectionsModal && renderDataConnectionsModal()}
{
const queryParams = new URLSearchParams(search)
const dispatch = useDispatch();
const [isCustodial, setIsCustodial] = useState();
- const allowedProviderNames = ['dexcom'];
const [authStatus, setAuthStatus] = useState();
const statusContent = {
@@ -64,7 +64,7 @@ export const OAuthConnection = (props) => {
const custodialSignup = queryParams.has('signupEmail') && queryParams.has('signupKey');
setIsCustodial(custodialSignup);
- if (includes(allowedProviderNames, providerName) && statusContent[status]) {
+ if (includes(activeProviders, providerName) && statusContent[status]) {
setAuthStatus(statusContent[status]);
} else {
setAuthStatus(statusContent.error)
diff --git a/app/redux/actions/async.js b/app/redux/actions/async.js
index e89bc43788..aa6a6ff910 100644
--- a/app/redux/actions/async.js
+++ b/app/redux/actions/async.js
@@ -2800,22 +2800,24 @@ export function revertClinicPatientLastReviewed(api, clinicId, patientId) {
}
/**
- * Send a dexcom connect reqeust email to a clinic patient
+ * Send a data source connection request email to a clinic patient
*
* @param {Object} api - an instance of the API wrapper
- * @param {String} clinicId - Id of the clinic
+ * @param {String} clinicId - clinic Id
+ * @param {String} patientId - id of the patient to send the data source connect request to
+ * @param {String} providerName - name of the provider to send the data source connect request to
*/
-export function sendPatientDexcomConnectRequest(api, clinicId, patientId) {
+export function sendPatientDataProviderConnectRequest(api, clinicId, patientId, providerName) {
return (dispatch) => {
- dispatch(sync.sendPatientDexcomConnectRequestRequest());
+ dispatch(sync.sendPatientDataProviderConnectRequestRequest());
- api.clinics.sendPatientDexcomConnectRequest(clinicId, patientId, (err, result) => {
+ api.clinics.sendPatientDataProviderConnectRequest(clinicId, patientId, providerName, err => {
if (err) {
- dispatch(sync.sendPatientDexcomConnectRequestFailure(
- createActionError(ErrorMessages.ERR_SENDING_PATIENT_DEXCOM_CONNECT_REQUEST, err), err
+ dispatch(sync.sendPatientDataProviderConnectRequestFailure(
+ createActionError(ErrorMessages.ERR_SENDING_PATIENT_DATA_PROVIDER_CONNECT_REQUEST, err), err
));
} else {
- dispatch(sync.sendPatientDexcomConnectRequestSuccess(clinicId, patientId, _.get(result, 'lastRequestedDexcomConnectTime', moment().toISOString())));
+ dispatch(sync.sendPatientDataProviderConnectRequestSuccess(clinicId, patientId, providerName, moment.utc().toISOString()));
}
});
};
diff --git a/app/redux/actions/sync.js b/app/redux/actions/sync.js
index b68dd8f4bf..3767ade4f7 100644
--- a/app/redux/actions/sync.js
+++ b/app/redux/actions/sync.js
@@ -2104,26 +2104,27 @@ export function revertClinicPatientLastReviewedFailure(error, apiError) {
};
}
-export function sendPatientDexcomConnectRequestRequest() {
+export function sendPatientDataProviderConnectRequestRequest() {
return {
- type: ActionTypes.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST,
+ type: ActionTypes.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST,
};
}
-export function sendPatientDexcomConnectRequestSuccess(clinicId, patientId, lastRequestedDexcomConnectTime) {
+export function sendPatientDataProviderConnectRequestSuccess(clinicId, patientId, providerName, createdTime) {
return {
- type: ActionTypes.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS,
+ type: ActionTypes.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS,
payload: {
clinicId: clinicId,
patientId: patientId,
- lastRequestedDexcomConnectTime: lastRequestedDexcomConnectTime,
+ providerName: providerName,
+ createdTime: createdTime,
},
};
}
-export function sendPatientDexcomConnectRequestFailure(error, apiError) {
+export function sendPatientDataProviderConnectRequestFailure(error, apiError) {
return {
- type: ActionTypes.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE,
+ type: ActionTypes.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE,
error: error,
meta: {
apiError: apiError || null,
diff --git a/app/redux/constants/actionTypes.js b/app/redux/constants/actionTypes.js
index 7838aa4c46..f1887f302b 100644
--- a/app/redux/constants/actionTypes.js
+++ b/app/redux/constants/actionTypes.js
@@ -477,10 +477,10 @@ export const REVERT_CLINIC_PATIENT_LAST_REVIEWED_REQUEST = 'REVERT_CLINIC_PATIEN
export const REVERT_CLINIC_PATIENT_LAST_REVIEWED_SUCCESS = 'REVERT_CLINIC_PATIENT_LAST_REVIEWED_SUCCESS';
export const REVERT_CLINIC_PATIENT_LAST_REVIEWED_FAILURE = 'REVERT_CLINIC_PATIENT_LAST_REVIEWED_FAILURE';
-// clinics.sendPatientDexcomConnectRequest
-export const SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST = 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST';
-export const SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS = 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS';
-export const SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE = 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE';
+// clinics.sendPatientDataProviderConnectRequest
+export const SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST = 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST';
+export const SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS = 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS';
+export const SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE = 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE';
// clinics.createClinicPatientTag
export const CREATE_CLINIC_PATIENT_TAG_REQUEST = 'CREATE_CLINIC_PATIENT_TAG_REQUEST';
diff --git a/app/redux/constants/actionWorkingMap.js b/app/redux/constants/actionWorkingMap.js
index 03d49b9f5b..c835840deb 100644
--- a/app/redux/constants/actionWorkingMap.js
+++ b/app/redux/constants/actionWorkingMap.js
@@ -394,10 +394,10 @@ export default (type) => {
case types.SEND_PATIENT_UPLOAD_REMINDER_FAILURE:
return 'sendingPatientUploadReminder';
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST:
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS:
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE:
- return 'sendingPatientDexcomConnectRequest';
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST:
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS:
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE:
+ return 'sendingPatientDataProviderConnectRequest';
case types.CREATE_CLINIC_PATIENT_TAG_REQUEST:
case types.CREATE_CLINIC_PATIENT_TAG_SUCCESS:
diff --git a/app/redux/constants/errorMessages.js b/app/redux/constants/errorMessages.js
index 833747c301..74006a5075 100644
--- a/app/redux/constants/errorMessages.js
+++ b/app/redux/constants/errorMessages.js
@@ -125,7 +125,7 @@ export const ERR_DISMISSING_CLINICIAN_INVITE = t('Something went wrong while dis
export const ERR_FETCHING_CLINICS_FOR_CLINICIAN = t('Something went wrong while getting clinics for clinician.');
export const ERR_TRIGGERING_INITIAL_CLINIC_MIGRATION = t('Something went wrong while migrating this clinic.');
export const ERR_SENDING_PATIENT_UPLOAD_REMINDER = t('Something went wrong while sending an upload reminder to the patient.');
-export const ERR_SENDING_PATIENT_DEXCOM_CONNECT_REQUEST = t('Something went wrong while sending a dexcom connect request to the patient.');
+export const ERR_SENDING_PATIENT_DATA_PROVIDER_CONNECT_REQUEST = t('Something went wrong while sending a data provider connect request to the patient.');
export const ERR_CREATING_CLINIC_PATIENT_TAG = t('Something went wrong while creating the patient tag.');
export const ERR_CREATING_CLINIC_PATIENT_TAG_MAX_EXCEEDED = t('Sorry, you already have the maximum number of patient tags.');
export const ERR_CREATING_CLINIC_PATIENT_TAG_DUPLICATE = t('Sorry, you already have a tag with that name.');
diff --git a/app/redux/reducers/initialState.js b/app/redux/reducers/initialState.js
index 81fa4283e1..10db8a251e 100644
--- a/app/redux/reducers/initialState.js
+++ b/app/redux/reducers/initialState.js
@@ -149,7 +149,7 @@ const initialState = {
fetchingClinicsForClinician: Object.assign({}, working),
triggeringInitialClinicMigration: Object.assign({}, working),
sendingPatientUploadReminder: Object.assign({}, working),
- sendingPatientDexcomConnectRequest: Object.assign({}, working),
+ sendingPatientDataProviderConnectRequest: Object.assign({}, working),
creatingClinicPatientTag: Object.assign({}, working),
updatingClinicPatientTag: Object.assign({}, working),
deletingClinicPatientTag: Object.assign({}, working),
diff --git a/app/redux/reducers/misc.js b/app/redux/reducers/misc.js
index 59b540283a..5542771aa5 100644
--- a/app/redux/reducers/misc.js
+++ b/app/redux/reducers/misc.js
@@ -982,18 +982,34 @@ export const clinics = (state = initialState.clinics, action) => {
},
});
}
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS: {
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS: {
const {
clinicId,
patientId,
- lastRequestedDexcomConnectTime,
+ providerName,
+ createdTime,
} = action.payload;
+ const patient = state[clinicId].patients[patientId];
+
+ const connectionRequest = {
+ createdTime: createdTime,
+ providerName: providerName,
+ };
+
+ const updatedProviderConnectionRequests = [
+ connectionRequest,
+ ...(patient.connectionRequests?.[providerName] || []),
+ ];
+
return update(state, {
[clinicId]: {
patients: { [patientId]: { $set: {
- ...state[clinicId].patients[patientId],
- lastRequestedDexcomConnectTime,
+ ...patient,
+ connectionRequests: {
+ ...patient.connectionRequests,
+ [providerName]: updatedProviderConnectionRequests,
+ }
} } },
},
});
diff --git a/app/redux/reducers/working.js b/app/redux/reducers/working.js
index f5aef0f58f..1c86f323c2 100644
--- a/app/redux/reducers/working.js
+++ b/app/redux/reducers/working.js
@@ -123,7 +123,7 @@ export default (state = initialWorkingState, action) => {
case types.SEND_PATIENT_UPLOAD_REMINDER_REQUEST:
case types.SET_CLINIC_PATIENT_LAST_REVIEWED_REQUEST:
case types.REVERT_CLINIC_PATIENT_LAST_REVIEWED_REQUEST:
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST:
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST:
case types.CREATE_CLINIC_PATIENT_TAG_REQUEST:
case types.UPDATE_CLINIC_PATIENT_TAG_REQUEST:
case types.DELETE_CLINIC_PATIENT_TAG_REQUEST:
@@ -180,7 +180,7 @@ export default (state = initialWorkingState, action) => {
types.SEND_PATIENT_UPLOAD_REMINDER_REQUEST,
types.SET_CLINIC_PATIENT_LAST_REVIEWED_REQUEST,
types.REVERT_CLINIC_PATIENT_LAST_REVIEWED_REQUEST,
- types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST,
+ types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST,
types.DATA_WORKER_REMOVE_DATA_REQUEST,
types.CREATE_CLINIC_PATIENT_TAG_REQUEST,
types.UPDATE_CLINIC_PATIENT_TAG_REQUEST,
@@ -302,7 +302,7 @@ export default (state = initialWorkingState, action) => {
case types.SEND_PATIENT_UPLOAD_REMINDER_SUCCESS:
case types.SET_CLINIC_PATIENT_LAST_REVIEWED_SUCCESS:
case types.REVERT_CLINIC_PATIENT_LAST_REVIEWED_SUCCESS:
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS:
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS:
case types.CREATE_CLINIC_PATIENT_TAG_SUCCESS:
case types.UPDATE_CLINIC_PATIENT_TAG_SUCCESS:
case types.DELETE_CLINIC_PATIENT_TAG_SUCCESS:
@@ -372,7 +372,7 @@ export default (state = initialWorkingState, action) => {
} else if (_.includes([
types.CREATE_CLINIC_CUSTODIAL_ACCOUNT_SUCCESS,
types.UPDATE_CLINIC_PATIENT_SUCCESS,
- types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS,
+ types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS,
types.ACCEPT_PATIENT_INVITATION_SUCCESS,
], action.type)) {
return update(state, {
@@ -485,7 +485,7 @@ export default (state = initialWorkingState, action) => {
case types.SEND_PATIENT_UPLOAD_REMINDER_FAILURE:
case types.SET_CLINIC_PATIENT_LAST_REVIEWED_FAILURE:
case types.REVERT_CLINIC_PATIENT_LAST_REVIEWED_FAILURE:
- case types.SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE:
+ case types.SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE:
case types.CREATE_CLINIC_PATIENT_TAG_FAILURE:
case types.UPDATE_CLINIC_PATIENT_TAG_FAILURE:
case types.DELETE_CLINIC_PATIENT_TAG_FAILURE:
diff --git a/app/themes/base/banners.js b/app/themes/base/banners.js
index 215932420b..e49005ab38 100644
--- a/app/themes/base/banners.js
+++ b/app/themes/base/banners.js
@@ -12,7 +12,15 @@ export default ({ colors, fonts, fontSizes, fontWeights }) => {
fontSize: fontSizes[4],
},
+ '.title': {
+ display: 'block',
+ fontFamily: fonts.default,
+ fontSize: fontSizes[0],
+ fontWeight: fontWeights.bold,
+ },
+
'.message': {
+ display: 'block',
fontFamily: fonts.default,
fontSize: fontSizes[0],
fontWeight: fontWeights.medium,
diff --git a/app/themes/base/buttons.js b/app/themes/base/buttons.js
index a24b3143ea..e9233d2b6e 100644
--- a/app/themes/base/buttons.js
+++ b/app/themes/base/buttons.js
@@ -196,6 +196,19 @@ export default ({
textDecoration: 'none',
},
},
+ textPrimaryLink: {
+ ...defaultStyles,
+ ...textButtonStyles,
+ color: colors.purpleBright,
+ display: 'inline-flex !important',
+ px: 1,
+ py: 1,
+ textDecoration: 'underline',
+ '&:hover,&:active': {
+ color: colors.text.primary,
+ textDecoration: 'underline',
+ },
+ },
textSecondary: {
...defaultStyles,
...textButtonStyles,
diff --git a/app/themes/baseTheme.js b/app/themes/baseTheme.js
index 7520f430be..b18447f809 100755
--- a/app/themes/baseTheme.js
+++ b/app/themes/baseTheme.js
@@ -291,6 +291,8 @@ const text = {
const styles = {
a: linkVariants.default,
+ hr: { borderBottom: borders.divider },
+ dividerDark: { borderBottom: borders.dividerDark },
};
export default {
diff --git a/app/themes/colorPalette.js b/app/themes/colorPalette.js
index 156421b957..9d11e76617 100644
--- a/app/themes/colorPalette.js
+++ b/app/themes/colorPalette.js
@@ -72,7 +72,6 @@ export default {
'#5A5A5A',
'#505050',
'#3E3E3E',
- '#707070'
],
greens: [
'#E6F8ED',
diff --git a/package.json b/package.json
index 9e4c020ad9..871790a983 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"build-config": "NODE_ENV=production node buildconfig",
"server": "node server",
"update-translations": "i18next 'app/**/*.js' 'node_modules/tideline/{plugins,js}/**/*.js' \"${TIDEPOOL_DOCKER_VIZ_DIR:-'../viz'}/{storiesDatatypes,storybookDatatypes,stories,src}/**/*.js\" -c i18next-parser.config.json -o .",
- "storybook": "yarn sb dev -c storybook -p 6006 --ci",
+ "storybook": "NODE_ENV=test yarn sb dev -p 6006 --ci",
"build-storybook": "build-storybook",
"lint": "yarn eslint app stories test"
},
@@ -64,7 +64,7 @@
"@storybook/react": "7.5.0",
"@storybook/react-webpack5": "7.5.0",
"@testing-library/react-hooks": "8.0.1",
- "@tidepool/viz": "1.44.0-web-3179-dep-updates.3",
+ "@tidepool/viz": "1.45.0-web-3346-summary-accuracy.4",
"async": "2.6.4",
"autoprefixer": "10.4.16",
"babel-core": "7.0.0-bridge.0",
@@ -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",
+ "tidepool-platform-client": "0.62.0-web-3272-patient-data-linking-after-creation.1",
"tidepool-standard-action": "0.1.1",
"ua-parser-js": "1.0.36",
"url-loader": "4.1.1",
@@ -205,7 +205,8 @@
"@emotion/styled": "11.3.0",
"crypto-js": "4.2.0",
"lodash": "4.17.21",
+ "qs": "6.11.2",
"react-sizeme": "2.6.12",
- "qs": "6.11.2"
+ "webpack": "5.94.0"
}
}
diff --git a/stories/Accordion.stories.js b/stories/Accordion.stories.js
index 10032a0b0b..5ff12f5623 100644
--- a/stories/Accordion.stories.js
+++ b/stories/Accordion.stories.js
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, Flex } from 'theme-ui';
-import { boolean } from '@storybook/addon-knobs';
+import { boolean as bool } from '@storybook/addon-knobs';
import baseTheme from '../app/themes/baseTheme';
import Accordion from '../app/components/elements/Accordion';
@@ -14,7 +14,7 @@ export default {
export const AccordionStory = {
render: () => {
- const initiallyExpanded = () => boolean('Panel 2 Initially Expanded', true);
+ const initiallyExpanded = () => bool('Panel 2 Initially Expanded', true);
const [expanded, setExpanded] = React.useState(initiallyExpanded());
const handleChange = (event, isExpanded) => setExpanded(isExpanded);
diff --git a/stories/Banner.stories.js b/stories/Banner.stories.js
index 97ae70d181..088b47add7 100644
--- a/stories/Banner.stories.js
+++ b/stories/Banner.stories.js
@@ -20,46 +20,41 @@ export default {
decorators: [withTheme],
};
-const bannerText = () =>
+const bannerTitleText = () =>
text(
- 'Banner Text',
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.'
- );
-const bannerTextDanger = () =>
- text(
- 'Banner Text Danger',
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.'
+ 'Banner Title Text',
+ 'Consectetur adipiscing elit, sed do eiusmod tempor.'
);
-const bannerTextWarning = () =>
- text(
- 'Banner Text Warning',
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.'
- );
-const bannerTextSuccess = () =>
+
+const bannerMessageText = () =>
text(
- 'Banner Text Success',
+ 'Banner Message Text',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.'
);
-function createBanner(message, variant, dismissable = true, actionText = '') {
- return { message, variant, dismissable, actionText };
+function createBanner(message, title, variant, dismissable = true, actionText = '') {
+ return { message, title, variant, dismissable, actionText };
}
export const BannerStory = {
render: () => {
const [alerts, setAlerts] = useState([
- createBanner(bannerText(), 'info'),
- createBanner(bannerText(), 'info', false),
- createBanner(bannerText(), 'info', true, 'Info Action'),
- createBanner(bannerTextWarning(), 'warning'),
- createBanner(bannerTextWarning(), 'warning', false),
- createBanner(bannerTextWarning(), 'warning', true, 'Warning Action'),
- createBanner(bannerTextDanger(), 'danger'),
- createBanner(bannerTextDanger(), 'danger', false),
- createBanner(bannerTextDanger(), 'danger', true, 'Danger Action'),
- createBanner(bannerTextSuccess(), 'success'),
- createBanner(bannerTextSuccess(), 'success', false),
- createBanner(bannerTextSuccess(), 'success', true, 'Success Action'),
+ createBanner(bannerMessageText(), null, 'info'),
+ createBanner(bannerMessageText(), null, 'info', false),
+ createBanner(bannerMessageText(), null, 'info', true, 'Info Action'),
+ createBanner(bannerMessageText(), bannerTitleText(), 'info', false),
+ createBanner(bannerMessageText(), null, 'warning'),
+ createBanner(bannerMessageText(), null, 'warning', false),
+ createBanner(bannerMessageText(), null, 'warning', true, 'Warning Action'),
+ createBanner(bannerMessageText(), bannerTitleText(), 'warning', false),
+ createBanner(bannerMessageText(), null, 'danger'),
+ createBanner(bannerMessageText(), null, 'danger', false),
+ createBanner(bannerMessageText(), null, 'danger', true, 'Danger Action'),
+ createBanner(bannerMessageText(), bannerTitleText(), 'danger', false),
+ createBanner(bannerMessageText(), null, 'success'),
+ createBanner(bannerMessageText(), null, 'success', false),
+ createBanner(bannerMessageText(), null, 'success', true, 'Success Action'),
+ createBanner(bannerMessageText(), bannerTitleText(), 'success', false),
]);
const handleDismissed = (index) => {
diff --git a/stories/Button.stories.js b/stories/Button.stories.js
index 113a990a5d..a41a9cad49 100644
--- a/stories/Button.stories.js
+++ b/stories/Button.stories.js
@@ -1,6 +1,6 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
-import { boolean, text } from '@storybook/addon-knobs';
+import { boolean as bool, text } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import KeyboardArrowDownRoundedIcon from '@material-ui/icons/KeyboardArrowDownRounded';
import OpenInNewRoundedIcon from '@material-ui/icons/OpenInNewRounded';
@@ -23,8 +23,8 @@ export default {
decorators: [withTheme],
};
-const disabled = () => boolean('Disabled', false);
-const processing = () => boolean('Processing', false);
+const disabled = () => bool('Disabled', false);
+const processing = () => bool('Processing', false);
export const Primary = {
render: () => {
@@ -148,7 +148,7 @@ export const Text = {
export const Filter = {
render: () => {
const buttonText = () => text('Button Text', 'Filter');
- const selected = () => boolean('Selected', false);
+ const selected = () => bool('Selected', false);
return (
{
- const defaultChecked = () => boolean('Default Checked', false);
- const disabled = () => boolean('Disable', false);
+ const defaultChecked = () => bool('Default Checked', false);
+ const disabled = () => bool('Disable', false);
const [isChecked, setChecked] = useState(defaultChecked());
const handleCheckbox = (e) => setChecked(e.target.checked);
const labelText = () => text('Label Text', 'Check Me');
- const error = () => boolean('Errored', false);
- const required = () => boolean('Required', false);
+ const error = () => bool('Errored', false);
+ const required = () => bool('Required', false);
return (
<>
diff --git a/stories/DataConnection.stories.js b/stories/DataConnection.stories.js
new file mode 100644
index 0000000000..f189d77c79
--- /dev/null
+++ b/stories/DataConnection.stories.js
@@ -0,0 +1,331 @@
+import React from 'react';
+import { action } from '@storybook/addon-actions';
+import { ThemeProvider } from '@emotion/react';
+import moment from 'moment-timezone';
+import map from 'lodash/map';
+import noop from 'lodash/noop';
+
+import baseTheme from '../app/themes/baseTheme';
+import { activeProviders, getDataConnectionProps } from '../app/components/datasources/DataConnections';
+import DataConnection from '../app/components/datasources/DataConnection';
+import PatientDetails from '../app/components/datasources/PatientDetails';
+import { Divider } from 'theme-ui';
+import { Subheading } from '../app/components/elements/FontStyles';
+import { clinicPatientFromAccountInfo } from '../app/core/personutils';
+import { reduce } from 'lodash';
+
+/* eslint-disable max-len */
+
+const withTheme = (Story) => (
+
+
+
+);
+
+export default {
+ title: 'DataConnection',
+ decorators: [withTheme],
+};
+
+const patientWithState = (isClinicContext, state, opts = {}) => ({
+ id: 'patient123',
+ dataSources: state ? map(activeProviders, providerName => ({
+ providerName,
+ state,
+ createdTime: opts.createdTime,
+ modifiedTime: opts.modifiedTime,
+ expirationTime: opts.expirationTime,
+ lastImportTime: opts.lastImportTime,
+ latestDataTime: opts.latestDataTime,
+ })) : undefined,
+ connectionRequests: isClinicContext && opts.createdTime ? reduce(activeProviders, (res, providerName) => {
+ res[providerName] = [{ providerName, createdTime: opts.createdTime }];
+ return res;
+ }, {}) : undefined,
+});
+
+const getDateInPast = (amount, unit) => moment.utc().subtract(amount, unit).toISOString();
+
+export const ClinicUser = {
+ render: () => {
+ const dataConnectionUnset = getDataConnectionProps(patientWithState(true), false, 'clinicID', noop);
+ const dataConnectionInviteJustSent = getDataConnectionProps(patientWithState(true, 'pending', { createdTime: getDateInPast(5, 'seconds') }), false, 'clinicID', noop);
+ const dataConnectionPending = getDataConnectionProps(patientWithState(true, 'pending', { createdTime: getDateInPast(5, 'days') }), false, 'clinicID', noop);
+ const dataConnectionPendingReconnect = getDataConnectionProps(patientWithState(true, 'pendingReconnect', { createdTime: getDateInPast(10, 'days') }), false, 'clinicID', noop);
+ const dataConnectionPendingExpired = getDataConnectionProps(patientWithState(true, 'pending', { createdTime: getDateInPast(31, 'days'), expirationTime: getDateInPast(1, 'days') }), false, 'clinicID', noop);
+ const dataConnectionConnected = getDataConnectionProps(patientWithState(true, 'connected'), false, 'clinicID', noop);
+ const dataConnectionDisconnected = getDataConnectionProps(patientWithState(true, 'disconnected', { modifiedTime: getDateInPast(7, 'hours') }), false, 'clinicID', noop);
+ const dataConnectionError = getDataConnectionProps(patientWithState(true, 'error', { modifiedTime: getDateInPast(20, 'minutes') }), false, 'clinicID', noop);
+ const dataConnectionUnknown = getDataConnectionProps(patientWithState(true, 'foo'), false, 'clinicID', noop);
+
+ return (
+
+ No Pending Connections
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionUnset[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Invite Just Sent
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionInviteJustSent[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Pending
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionPending[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Pending Reconnection
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionPendingReconnect[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Pending Expired
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionPendingExpired[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Connected
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionConnected[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Disconnected
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionDisconnected[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Error
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionError[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Unknown
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionUnknown[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+ );
+ },
+
+ name: 'Clinic User',
+
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/design/LdoOQCUyQKIS2d6fUhfFJx/Cloud-to-Cloud?node-id=2212-18679&node-type=section&t=qP2OFSYnWA1USfSp-0',
+ },
+ },
+};
+
+export const PatientUser = {
+ render: () => {
+ const dataConnectionUnset = getDataConnectionProps(patientWithState(false), true, null, noop);
+ const dataConnectionJustConnected = getDataConnectionProps(patientWithState(false, 'connected', { createdTime: getDateInPast(1, 'minutes') }), true, null, noop);
+ const dataConnectionConnectedWithNoData = getDataConnectionProps(patientWithState(false, 'connected', { lastImportTime: getDateInPast(5, 'minutes') }), true, null, noop);
+ const dataConnectionConnectedWithData = getDataConnectionProps(patientWithState(false, 'connected', { lastImportTime: getDateInPast(1, 'minutes'), latestDataTime: getDateInPast(35, 'minutes') }), true, null, noop);
+ const dataConnectionDisconnected = getDataConnectionProps(patientWithState(false, 'disconnected', { modifiedTime: getDateInPast(1, 'hour') }), true, null, noop);
+ const dataConnectionError = getDataConnectionProps(patientWithState(false, 'error', { modifiedTime: getDateInPast(6, 'days') }), true, null, noop);
+ const dataConnectionUnknown = getDataConnectionProps(patientWithState(false, 'foo'), true, null, noop);
+
+ return (
+
+ No Pending Connections
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionUnset[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Connected
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionJustConnected[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Connected With No Data
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionConnectedWithNoData[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Connected With Data
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionConnectedWithData[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Disconnected
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionDisconnected[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Error
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionError[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+
+ Unknown
+
+ {map(activeProviders, (provider, index) => (
+ action(dataConnectionUnknown[provider]?.buttonText)(provider) : undefined}
+ />
+ ))}
+
+ );
+ },
+
+ name: 'Patient User',
+
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/design/LdoOQCUyQKIS2d6fUhfFJx/Cloud-to-Cloud?node-id=2212-18679&node-type=section&t=qP2OFSYnWA1USfSp-0',
+ },
+ },
+};
+
+export const PatientDetailBar = {
+ render: () => {
+ const clinicPatient = {
+ fullName: 'Jonathan Jellyfish',
+ birthDate: '1984-08-24',
+ mrn: '123456',
+ };
+
+ const noMRNPatient = {
+ fullName: 'James Jellyfish',
+ birthDate: '1994-06-20',
+ };
+
+ const accountPatient = {
+ profile: {
+ fullName: 'Jill Jellyfish',
+ patient: {
+ birthday: '1988-07-04',
+ mrn: '654321',
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+ },
+
+ name: 'Patient Details',
+
+ parameters: {
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/design/LdoOQCUyQKIS2d6fUhfFJx/Cloud-to-Cloud?node-id=2212-18679&node-type=section&t=qP2OFSYnWA1USfSp-0',
+ },
+ },
+};
diff --git a/stories/Datepicker.stories.js b/stories/Datepicker.stories.js
index a77c8557aa..c38896e66f 100644
--- a/stories/Datepicker.stories.js
+++ b/stories/Datepicker.stories.js
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import moment from 'moment';
import WindowSizeListener from 'react-window-size-listener';
-import { boolean, date, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, date, optionsKnob as options } from '@storybook/addon-knobs';
import DatePicker from '../app/components/elements/DatePicker';
import DateRangePicker from '../app/components/elements/DateRangePicker';
@@ -23,7 +23,7 @@ export const DatePickerStory = {
return moment.utc(stringTimestamp);
};
- const getFocused = () => boolean('Initially Focused', true);
+ const getFocused = () => bool('Initially Focused', true);
return (
{
- const initiallyOpen = () => boolean('Initially Open', true);
+ const initiallyOpen = () => bool('Initially Open', true);
- const showTitle = () => boolean('Show Title', true);
- const showTitleClose = () => boolean('Show Close Icon', true);
+ const showTitle = () => bool('Show Title', true);
+ const showTitleClose = () => bool('Show Close Icon', true);
const titleText = () => text('Title Text', 'Dialog Title');
- const showContent = () => boolean('Show Content', true);
- const showDividers = () => boolean('Show Dividers', true);
+ const showContent = () => bool('Show Content', true);
+ const showDividers = () => bool('Show Dividers', true);
const numberOfParagraphs = () => number('Number of Paragraphs', 2, {});
const getParagraphs = () => {
@@ -56,8 +56,8 @@ export const DialogStory = {
return paragraphs;
};
- const showActions = () => boolean('Show Actions', true);
- const alertOnActions = () => boolean('Alert on Action', false);
+ const showActions = () => bool('Show Actions', true);
+ const alertOnActions = () => bool('Alert on Action', false);
const showAlert = alertOnActions();
const [open, setOpen] = useState(initiallyOpen());
diff --git a/stories/Icon.stories.js b/stories/Icon.stories.js
index 7bd1f7b118..e009cba1fe 100644
--- a/stories/Icon.stories.js
+++ b/stories/Icon.stories.js
@@ -1,7 +1,7 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
-import { boolean } from '@storybook/addon-knobs';
+import { boolean as bool } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import CloseRoundedIcon from '@material-ui/icons/CloseRounded';
import MoreHorizRoundedIcon from '@material-ui/icons/MoreHorizRounded';
@@ -24,7 +24,7 @@ export default {
decorators: [withTheme],
};
-const disabled = () => boolean('Disabled', false);
+const disabled = () => bool('Disabled', false);
export const Default = {
render: () => (
diff --git a/stories/Pagination.stories.js b/stories/Pagination.stories.js
index 838d5902d0..c29c8f2fcf 100644
--- a/stories/Pagination.stories.js
+++ b/stories/Pagination.stories.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { boolean, select, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, select, optionsKnob as options } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import range from 'lodash/range';
import { Text } from 'theme-ui';
@@ -24,9 +24,9 @@ export default {
const pageCount = () => select('Page Count', range(5, 1000), 1000);
const initialPage = () => select('Initial Page', range(1, pageCount()), 99);
-const disabled = () => boolean('Disabled', false);
-const showPrevNextControls = () => boolean('Show Prev/Next Controls', true);
-const showFirstLastControls = () => boolean('Show First/Last Controls', false);
+const disabled = () => bool('Disabled', false);
+const showPrevNextControls = () => bool('Show Prev/Next Controls', true);
+const showFirstLastControls = () => bool('Show First/Last Controls', false);
const variants = {
Default: 'default',
diff --git a/stories/Popover.stories.js b/stories/Popover.stories.js
index 0a26aa7dec..9e07f4ab7c 100644
--- a/stories/Popover.stories.js
+++ b/stories/Popover.stories.js
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
-import { boolean } from '@storybook/addon-knobs';
+import { boolean as bool } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import { Text, Box, Flex } from 'theme-ui';
import DeleteForeverRoundedIcon from '@material-ui/icons/DeleteForeverRounded';
@@ -41,7 +41,7 @@ export default {
export const Simple = {
render: () => {
- const onHover = () => boolean('Trigger On Hover', false);
+ const onHover = () => bool('Trigger On Hover', false);
const popupState = usePopupState({
variant: 'popover',
diff --git a/stories/RadioGroup.stories.js b/stories/RadioGroup.stories.js
index 09b7023143..e27a46f24f 100644
--- a/stories/RadioGroup.stories.js
+++ b/stories/RadioGroup.stories.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import { boolean, text, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, text, optionsKnob as options } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import mapValues from 'lodash/mapValues';
import keyBy from 'lodash/keyBy';
@@ -33,9 +33,9 @@ export const RadioGroupStory = {
const label = () => text('Label', 'Group Label');
const defaultValue = () =>
options('Default Value', knobOptions, 'two', { display: 'inline-radio' });
- const disabled = () => boolean('Disabled', false);
- const error = () => boolean('Errored', false);
- const required = () => boolean('Required', false);
+ const disabled = () => bool('Disabled', false);
+ const error = () => bool('Errored', false);
+ const required = () => bool('Required', false);
const orientations = {
Horizontal: 'horizontal',
diff --git a/stories/Select.stories.js b/stories/Select.stories.js
index 7611dde423..ff3d5db9e9 100644
--- a/stories/Select.stories.js
+++ b/stories/Select.stories.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import { boolean, text, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, text, optionsKnob as options } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import mapValues from 'lodash/mapValues';
import keyBy from 'lodash/keyBy';
@@ -36,9 +36,9 @@ export const Simple = {
const label = () => text('Label', 'Field Label');
const defaultValue = () =>
options('Default Value', knobOptions, 'two', { display: 'inline-radio' });
- const disabled = () => boolean('Disabled', false);
- const error = () => boolean('Errored', false);
- const required = () => boolean('Required', false);
+ const disabled = () => bool('Disabled', false);
+ const error = () => bool('Errored', false);
+ const required = () => bool('Required', false);
const variants = {
Default: 'default',
diff --git a/stories/TabGroup.stories.js b/stories/TabGroup.stories.js
index 231eea563f..9573a40223 100644
--- a/stories/TabGroup.stories.js
+++ b/stories/TabGroup.stories.js
@@ -1,6 +1,6 @@
import React from 'react';
-import { boolean, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, optionsKnob as options } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import { Box } from 'theme-ui';
import MoreHorizRoundedIcon from '@material-ui/icons/MoreHorizRounded';
@@ -24,7 +24,7 @@ export default {
decorators: [withTheme],
};
-const tabDisabled = (i) => boolean(`Tab ${i + 1} Disabled`, false);
+const tabDisabled = (i) => bool(`Tab ${i + 1} Disabled`, false);
const orientations = {
Horizontal: 'horizontal',
diff --git a/stories/Table.stories.js b/stories/Table.stories.js
index a49bdf8a27..7f4173f4b0 100644
--- a/stories/Table.stories.js
+++ b/stories/Table.stories.js
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
-import { boolean, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, optionsKnob as options } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import toUpper from 'lodash/toUpper';
import random from 'lodash/random';
@@ -179,8 +179,8 @@ const data = [
createData(createPatient('Marco Manowar', 'marco@testemail.com'), 'success', '', 'Clinic Admin'),
];
-const stickyHeader = () => boolean('Sticky Header', false);
-const rowHover = () => boolean('Enable Row Hover', true);
+const stickyHeader = () => bool('Sticky Header', false);
+const rowHover = () => bool('Enable Row Hover', true);
const variants = {
Default: 'default',
diff --git a/stories/TextInput.stories.js b/stories/TextInput.stories.js
index 6b814eff3c..3f887d5750 100644
--- a/stories/TextInput.stories.js
+++ b/stories/TextInput.stories.js
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { boolean, text, number, optionsKnob as options } from '@storybook/addon-knobs';
+import { boolean as bool, text, number, optionsKnob as options } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import baseTheme from '../app/themes/baseTheme';
@@ -21,12 +21,12 @@ export default {
const label = () => text('Label', 'Name');
const width = () => number('Width');
-const disabled = () => boolean('Disabled', false);
+const disabled = () => bool('Disabled', false);
const placeholder = () => text('Placeholder', 'Your name');
-const error = () => boolean('Errored State', false);
-const warning = () => boolean('Warning State', false);
-const required = () => boolean('Required', false);
-const icon = () => boolean('Icon', false);
+const error = () => bool('Errored State', false);
+const warning = () => bool('Warning State', false);
+const required = () => bool('Required', false);
+const icon = () => bool('Icon', false);
const suffix = () => text('Suffix', '');
const prefix = () => text('Prefix', '');
diff --git a/stories/Toggle.stories.js b/stories/Toggle.stories.js
index 6b3b86e9f4..06f41a870f 100644
--- a/stories/Toggle.stories.js
+++ b/stories/Toggle.stories.js
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { boolean } from '@storybook/addon-knobs';
+import { boolean as bool } from '@storybook/addon-knobs';
import { ThemeProvider } from '@emotion/react';
import { Switch } from 'theme-ui';
@@ -18,8 +18,8 @@ export default {
export const ToggleStory = {
render: () => {
- const defaultChecked = () => boolean('Default Checked', true);
- const disabled = () => boolean('Disable', false);
+ const defaultChecked = () => bool('Default Checked', true);
+ const disabled = () => bool('Disable', false);
const [isChecked, setChecked] = useState(defaultChecked());
const handleToggle = () => setChecked(!isChecked);
diff --git a/test/unit/app/core/api.test.js b/test/unit/app/core/api.test.js
index 3c79d07ed9..68fb77ff2e 100644
--- a/test/unit/app/core/api.test.js
+++ b/test/unit/app/core/api.test.js
@@ -69,7 +69,7 @@ describe('api', () => {
getClinicByShareCode: sinon.stub(),
triggerInitialClinicMigration: sinon.stub(),
sendPatientUploadReminder: sinon.stub(),
- sendPatientDexcomConnectRequest: sinon.stub(),
+ sendPatientDataProviderConnectRequest: sinon.stub(),
createClinicPatientTag: sinon.stub(),
updateClinicPatientTag: sinon.stub(),
deleteClinicPatientTag: sinon.stub(),
@@ -142,7 +142,7 @@ describe('api', () => {
tidepool.getClinicByShareCode.resetHistory();
tidepool.triggerInitialClinicMigration.resetHistory();
tidepool.sendPatientUploadReminder.resetHistory();
- tidepool.sendPatientDexcomConnectRequest.resetHistory();
+ tidepool.sendPatientDataProviderConnectRequest.resetHistory();
tidepool.createClinicPatientTag.resetHistory();
tidepool.updateClinicPatientTag.resetHistory();
tidepool.deleteClinicPatientTag.resetHistory();
@@ -1055,13 +1055,14 @@ describe('api', () => {
sinon.assert.calledWith(tidepool.sendPatientUploadReminder, clinicId, patientId, cb);
});
});
- describe('clinics.sendPatientDexcomConnectRequest', () => {
- it('should call tidepool.sendPatientDexcomConnectRequest with the appropriate args', () => {
+ describe('clinics.sendPatientDataProviderConnectRequest', () => {
+ it('should call tidepool.sendPatientDataProviderConnectRequest with the appropriate args', () => {
const cb = sinon.stub();
const patientId = 'patientId123';
const clinicId = 'clinicId123';
- api.clinics.sendPatientDexcomConnectRequest(clinicId, patientId, cb);
- sinon.assert.calledWith(tidepool.sendPatientDexcomConnectRequest, clinicId, patientId, cb);
+ const providerName = 'dexcom';
+ api.clinics.sendPatientDataProviderConnectRequest(clinicId, patientId, providerName, cb);
+ sinon.assert.calledWith(tidepool.sendPatientDataProviderConnectRequest, clinicId, patientId, providerName, cb);
});
});
describe('clinics.createClinicPatientTag', () => {
diff --git a/test/unit/components/datasources/DataConnection.test.js b/test/unit/components/datasources/DataConnection.test.js
new file mode 100644
index 0000000000..699666ce2d
--- /dev/null
+++ b/test/unit/components/datasources/DataConnection.test.js
@@ -0,0 +1,82 @@
+import React from 'react';
+import { createMount } from '@material-ui/core/test-utils';
+import DataConnection from '../../../../app/components/datasources/DataConnection';
+
+/* global chai */
+/* global sinon */
+/* global describe */
+/* global it */
+/* global afterEach */
+
+const expect = chai.expect;
+
+describe('DataConnection', () => {
+ const mount = createMount();
+
+ let wrapper;
+ const defaultProps = {
+ label: 'test data connection',
+ buttonDisabled: false,
+ buttonHandler: sinon.stub(),
+ buttonProcessing: false,
+ buttonText: 'Click Me',
+ messageText: 'Some message',
+ stateText: 'The state',
+ };
+
+ afterEach(() => {
+ defaultProps.buttonHandler.resetHistory();
+ });
+
+ it('should render the data connection text with the provided props', () => {
+ wrapper = mount();
+
+ const state = wrapper.find('.state-text').hostNodes();
+ expect(state).to.have.lengthOf(1);
+ expect(state.text()).to.equal('The state');
+
+ const stateMessage = wrapper.find('.state-message').hostNodes();
+ expect(stateMessage).to.have.lengthOf(1);
+ expect(stateMessage.text()).to.equal(' - Some message');
+
+ const button = wrapper.find('.action').hostNodes();
+ expect(button).to.have.lengthOf(1);
+ expect(button.text()).to.equal('Click Me');
+ });
+
+ it('should call the button handler', () => {
+ wrapper = mount();
+
+ const button = wrapper.find('.action').hostNodes();
+ expect(button).to.have.lengthOf(1);
+
+ sinon.assert.notCalled(defaultProps.buttonHandler);
+ expect(button.props().disabled).to.be.false;
+ expect(button.is('.processing')).to.be.false;
+ button.simulate('click');
+ sinon.assert.calledOnce(defaultProps.buttonHandler);
+ });
+
+ it('should not show the button if no button handler is provided', () => {
+ wrapper = mount();
+
+ const button = wrapper.find('.action').hostNodes();
+ expect(button).to.have.lengthOf(0);
+ });
+
+ it('should show a disabled button if dictated by prop', () => {
+ wrapper = mount();
+
+ const button = wrapper.find('.action').hostNodes();
+ expect(button).to.have.lengthOf(1);
+ expect(button.props().disabled).to.be.true;
+ });
+
+ it('should show a processing button if dictated by prop', () => {
+ wrapper = mount();
+
+ const button = wrapper.find('.action').hostNodes();
+ expect(button).to.have.lengthOf(1);
+ expect(button.is('.processing')).to.be.true;
+ });
+});
diff --git a/test/unit/components/datasources/DataConnections.test.js b/test/unit/components/datasources/DataConnections.test.js
new file mode 100644
index 0000000000..4d49511b26
--- /dev/null
+++ b/test/unit/components/datasources/DataConnections.test.js
@@ -0,0 +1,1306 @@
+import React from 'react';
+import moment from 'moment';
+import { createMount } from '@material-ui/core/test-utils';
+import configureStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+import { Button } from '../../../../app/components/elements/Button';
+import CheckRoundedIcon from '@material-ui/icons/CheckRounded';
+import { ToastProvider } from '../../../../app/providers/ToastProvider';
+import map from 'lodash/map';
+import reduce from 'lodash/reduce';
+
+import PatientEmailModal from '../../../../app/components/datasources/PatientEmailModal';
+
+import DataConnections, {
+ activeProviders,
+ providers,
+ getProviderHandlers,
+ getConnectStateUI,
+ getDataConnectionProps
+} from '../../../../app/components/datasources/DataConnections';
+
+/* global chai */
+/* global sinon */
+/* global describe */
+/* global context */
+/* global it */
+/* global beforeEach */
+/* global before */
+/* global afterEach */
+/* global after */
+
+const expect = chai.expect;
+const mockStore = configureStore([thunk]);
+
+describe('activeProviders', () => {
+ it('should define a list of active providers', () => {
+ expect(activeProviders).to.eql(['dexcom', 'abbott']);
+ });
+});
+
+describe('providers', () => {
+ it('should define the provider details', () => {
+ const { dexcom, abbott, twiist } = providers;
+
+ expect(dexcom.id).to.equal('oauth/dexcom');
+ expect(dexcom.displayName).to.equal('Dexcom');
+ expect(dexcom.restrictedTokenCreate).to.eql({ paths: ['/v1/oauth/dexcom'] });
+ expect(dexcom.dataSourceFilter).to.eql({ providerType: 'oauth', providerName: 'dexcom' });
+ expect(dexcom.logoImage).to.be.a('string');
+
+ expect(abbott.id).to.equal('oauth/abbott');
+ expect(abbott.displayName).to.equal('Freestyle Libre');
+ expect(abbott.restrictedTokenCreate).to.eql({ paths: ['/v1/oauth/abbott'] });
+ expect(abbott.dataSourceFilter).to.eql({ providerType: 'oauth', providerName: 'abbott' });
+ expect(abbott.logoImage).to.be.a('string');
+
+ expect(twiist.id).to.equal('oauth/twiist');
+ expect(twiist.displayName).to.equal('Twiist');
+ expect(twiist.restrictedTokenCreate).to.eql({ paths: ['/v1/oauth/twiist'] });
+ expect(twiist.dataSourceFilter).to.eql({ providerType: 'oauth', providerName: 'twiist' });
+ expect(twiist.logoImage).to.be.a('string');
+ });
+});
+
+describe('getProviderHandlers', () => {
+ const actions = {
+ async: {
+ connectDataSource: 'connectDataSourceStub',
+ disconnectDataSource: 'disconnectDataSourceStub',
+ sendPatientDataProviderConnectRequest: 'sendPatientDataProviderConnectRequestStub',
+ }
+ };
+
+ const api = 'api123';
+
+ beforeEach(() => {
+ DataConnections.__Rewire__('actions', actions);
+ DataConnections.__Rewire__('api', api);
+ });
+
+ afterEach(() => {
+ DataConnections.__ResetDependency__('actions');
+ DataConnections.__ResetDependency__('api');
+ });
+
+ it('should define the default action handlers for a given provider and patient', () => {
+ const patient = { id: 'patient123', email: 'patient@123.com', dataSources: [ { providerName: 'provider123' }] };
+ const selectedClinicId = 'clinic123';
+ const provider = { id: 'oauth/provider123', dataSourceFilter: { providerName: 'provider123' }, restrictedTokenCreate: { paths: ['/v1/oauth/provider123'] }};
+
+ expect(getProviderHandlers(patient, selectedClinicId, provider)).to.eql({
+ connect: {
+ buttonText: 'Connect',
+ buttonStyle: 'solid',
+ action: 'connectDataSourceStub',
+ args: ['api123', 'oauth/provider123', provider.restrictedTokenCreate, provider.dataSourceFilter],
+ },
+ disconnect: {
+ buttonText: 'Disconnect',
+ buttonStyle: 'text',
+ action: 'disconnectDataSourceStub',
+ args: ['api123', 'oauth/provider123', provider.dataSourceFilter],
+ },
+ inviteSent: {
+ buttonDisabled: true,
+ buttonIcon: CheckRoundedIcon,
+ buttonText: 'Invite Sent',
+ buttonStyle: 'staticText',
+ action: 'connectDataSourceStub',
+ args: ['api123', 'oauth/provider123', provider.restrictedTokenCreate, provider.dataSourceFilter],
+ },
+ reconnect: {
+ buttonText: 'Reconnect',
+ buttonStyle: 'solid',
+ action: 'connectDataSourceStub',
+ args: ['api123', 'oauth/provider123', provider.restrictedTokenCreate, provider.dataSourceFilter],
+ },
+ sendInvite: {
+ buttonText: 'Email Invite',
+ buttonStyle: 'solid',
+ action: 'sendPatientDataProviderConnectRequestStub',
+ args: ['api123', 'clinic123', 'patient123', 'provider123'],
+ emailRequired: false,
+ patientUpdates: undefined,
+ },
+ resendInvite: {
+ buttonText: 'Resend Invite',
+ buttonStyle: 'solid',
+ action: 'sendPatientDataProviderConnectRequestStub',
+ args: ['api123', 'clinic123', 'patient123', 'provider123'],
+ emailRequired: false,
+ patientUpdates: undefined,
+ },
+ });
+ });
+
+ it('should set emailRequired to true for send and resend invite actions if email is missing on a clinic custodial patient', () => {
+ const patient = { id: 'patient123', email: null, permissions: { custodian: {} }, dataSources: [ { providerName: 'provider123' }] };
+ const selectedClinicId = 'clinic123';
+ const provider = { id: 'oauth/provider123', dataSourceFilter: { providerName: 'provider123' }, restrictedTokenCreate: { paths: ['/v1/oauth/provider123'] }};
+
+ const handlers = getProviderHandlers(patient, selectedClinicId, provider);
+ expect(handlers.sendInvite.emailRequired).to.be.true;
+ expect(handlers.resendInvite.emailRequired).to.be.true;
+ });
+
+ it('should set patientUpdates to include pending data source for send and resend invite actions if patient does not have a data source for the provider', () => {
+ const patient = { id: 'patient123', email: 'patient@123.com', permissions: { custodian: {} }, dataSources: [ { providerName: 'otherProvider' }] };
+ const selectedClinicId = 'clinic123';
+ const provider = { id: 'oauth/provider123', dataSourceFilter: { providerName: 'provider123' }, restrictedTokenCreate: { paths: ['/v1/oauth/provider123'] }};
+
+ const handlers = getProviderHandlers(patient, selectedClinicId, provider);
+ expect(handlers.sendInvite.patientUpdates).to.eql({ dataSources: [ { providerName: 'otherProvider' }, { providerName: 'provider123', state: 'pending' } ]});
+ expect(handlers.resendInvite.patientUpdates).to.eql({ dataSources: [ { providerName: 'otherProvider' }, { providerName: 'provider123', state: 'pending' } ]});
+ });
+});
+
+describe('getConnectStateUI', () => {
+ const clinicPatient = {
+ id: 'patient123',
+ dataSources: [ { providerName: 'provider123', state: 'pending' }],
+ connectionRequests: { provider123: [{ createdTime: moment.utc().subtract(20, 'days') }] },
+ };
+
+ const clinicPatientJustSent = {
+ ...clinicPatient,
+ connectionRequests: { provider123: [{ createdTime: moment.utc().subtract(30, 'seconds') }] },
+ }
+
+ const userPatient = {
+ id: 'patient123',
+ dataSources: [ {
+ providerName: 'provider123',
+ state: 'pending',
+ createdTime: moment.utc().subtract(20, 'days'),
+ }],
+ }
+
+ const userPatientNoDataFound = {
+ id: 'patient123',
+ dataSources: [ {
+ providerName: 'provider123',
+ state: 'pending',
+ createdTime: moment.utc().subtract(20, 'days'),
+ lastImportTime: moment.utc().subtract(10, 'days'),
+ }],
+ }
+
+ const userPatientDataFound = {
+ id: 'patient123',
+ dataSources: [ {
+ providerName: 'provider123',
+ state: 'pending',
+ createdTime: moment.utc().subtract(20, 'days'),
+ lastImportTime: moment.utc().subtract(10, 'days'),
+ latestDataTime: moment.utc().subtract(5, 'days'),
+ }],
+ }
+
+ context('clinician user', () => {
+ it('should define the UI props for the various connection states', () => {
+ const UI = getConnectStateUI(clinicPatient, false, 'provider123');
+ const UIJustSent = getConnectStateUI(clinicPatientJustSent, false, 'provider123');
+
+ expect(UI.noPendingConnections.message).to.equal(null);
+ expect(UI.noPendingConnections.text).to.equal('No Pending Connections');
+ expect(UI.noPendingConnections.handler).to.equal('sendInvite');
+
+ expect(UI.inviteJustSent.message).to.equal(null);
+ expect(UI.inviteJustSent.text).to.equal('Connection Pending');
+ expect(UI.inviteJustSent.handler).to.equal('inviteSent');
+
+ expect(UI.pending.message).to.equal('Invite sent 20 days ago');
+ expect(UIJustSent.pending.message).to.equal('Invite sent a few seconds ago');
+ expect(UI.pending.text).to.equal('Connection Pending');
+ expect(UI.pending.handler).to.equal('resendInvite');
+ expect(UI.pending.inviteJustSent).to.be.undefined;
+ expect(UIJustSent.pending.inviteJustSent).to.be.true;
+
+ expect(UI.pendingReconnect.message).to.equal('Invite sent 20 days ago');
+ expect(UIJustSent.pendingReconnect.message).to.equal('Invite sent a few seconds ago');
+ expect(UI.pendingReconnect.text).to.equal('Invite Sent');
+ expect(UI.pendingReconnect.handler).to.equal('resendInvite');
+ expect(UI.pendingReconnect.inviteJustSent).to.be.undefined;
+ expect(UIJustSent.pendingReconnect.inviteJustSent).to.be.true;
+
+ expect(UI.pendingExpired.message).to.equal('Sent over one month ago');
+ expect(UI.pendingExpired.text).to.equal('Invite Expired');
+ expect(UI.pendingExpired.handler).to.equal('resendInvite');
+
+ expect(UI.connected.message).to.equal(null);
+ expect(UI.connected.text).to.equal('Connected');
+ expect(UI.connected.handler).to.equal(null);
+
+ expect(UI.disconnected.message).to.equal('Last update 20 days ago');
+ expect(UI.disconnected.text).to.equal('Patient Disconnected');
+ expect(UI.disconnected.handler).to.equal('resendInvite');
+
+ expect(UI.error.message).to.equal('Last update 20 days ago');
+ expect(UI.error.text).to.equal('Error Connecting');
+ expect(UI.error.handler).to.equal('resendInvite');
+ });
+ });
+
+ context('patient user', () => {
+ it('should define the UI props for the various connection states', () => {
+ const UI = getConnectStateUI(userPatient, true, 'provider123');
+ const UINoDataFound = getConnectStateUI(userPatientNoDataFound, true, 'provider123');
+ const UIDataFound = getConnectStateUI(userPatientDataFound, true, 'provider123');
+
+ expect(UI.noPendingConnections.message).to.equal(null);
+ expect(UI.noPendingConnections.text).to.equal(null);
+ expect(UI.noPendingConnections.handler).to.equal('connect');
+
+ expect(UI.inviteJustSent.message).to.equal(null);
+ expect(UI.inviteJustSent.text).to.equal('Connection Pending');
+ expect(UI.inviteJustSent.handler).to.equal('inviteSent');
+
+ expect(UI.pending.message).to.equal('Invite sent 20 days ago');
+ expect(UI.pending.text).to.equal('Connection Pending');
+ expect(UI.pending.handler).to.equal('connect');
+ expect(UI.pending.inviteJustSent).to.be.undefined;
+
+ expect(UI.pendingReconnect.message).to.equal('Invite sent 20 days ago');
+ expect(UI.pendingReconnect.text).to.equal('Invite Sent');
+ expect(UI.pendingReconnect.handler).to.equal('connect');
+ expect(UI.pendingReconnect.inviteJustSent).to.be.undefined;
+
+ expect(UI.pendingExpired.message).to.equal('Sent over one month ago');
+ expect(UI.pendingExpired.text).to.equal('Invite Expired');
+ expect(UI.pendingExpired.handler).to.equal('connect');
+
+ expect(UI.connected.message).to.equal('This can take a few minutes');
+ expect(UI.connected.text).to.equal('Connecting');
+ expect(UI.connected.handler).to.equal('disconnect');
+ expect(UINoDataFound.connected.message).to.equal('No data found as of 10 days ago');
+ expect(UINoDataFound.connected.text).to.equal('Connected');
+ expect(UINoDataFound.connected.handler).to.equal('disconnect');
+ expect(UIDataFound.connected.text).to.equal('Connected');
+ expect(UIDataFound.connected.message).to.equal('Last data 5 days ago');
+ expect(UIDataFound.connected.handler).to.equal('disconnect');
+
+ expect(UI.disconnected.message).to.equal(null);
+ expect(UI.disconnected.text).to.equal(null);
+ expect(UI.disconnected.handler).to.equal('connect');
+
+ expect(UI.error.message).to.equal('Last update 20 days ago. Please reconnect your account to keep syncing data.');
+ expect(UI.error.text).to.equal('Error Connecting');
+ expect(UI.error.handler).to.equal('reconnect');
+ });
+ });
+});
+
+describe('getDataConnectionProps', () => {
+ const setActiveHandlerStub = sinon.stub();
+
+ const connectStates = [
+ 'connectState1',
+ 'connectState2',
+ ];
+
+ const createPatientWithConnectionState = state => ({
+ id: 'patient123',
+ dataSources: [ { providerName: 'provider123', state }],
+ connectionRequests: { provider123: [{ createdTime: moment.utc().subtract(20, 'days') }] },
+ });
+
+ beforeEach(() => {
+ DataConnections.__Rewire__('getConnectStateUI', () => reduce(connectStates, (res, state) => {
+ return {
+ ...res,
+ [state]: {
+ color: `${state} color stub`,
+ icon: `${state} icon stub`,
+ message: `${state} message stub`,
+ text: `${state} text stub`,
+ handler: `${state} handler stub`,
+ },
+ };
+ }, {}));
+
+ DataConnections.__Rewire__('getProviderHandlers', () => reduce(connectStates, (res, state) => {
+ return {
+ ...res,
+ [`${state} handler stub`]: {
+ action: `${state} handler action stub`,
+ args: `${state} handler args stub`,
+ buttonDisabled: `${state} handler buttonDisabled stub`,
+ buttonIcon: `${state} handler buttonIcon stub`,
+ buttonText: `${state} handler buttonText stub`,
+ buttonStyle: `${state} handler buttonStyle stub`,
+ emailRequired: `${state} handler emailRequired stub`,
+ patientUpdates: `${state} handler patientUpdates stub`,
+ },
+ };
+ }, {}));
+
+ DataConnections.__Rewire__('activeProviders', ['provider123']);
+
+ DataConnections.__Rewire__('providers', {
+ provider123: {
+ logoImage: 'provider123 logo image stub',
+ }
+ });
+ });
+
+ afterEach(() => {
+ setActiveHandlerStub.resetHistory();
+ DataConnections.__ResetDependency__('getConnectStateUI');
+ DataConnections.__ResetDependency__('getProviderHandlers');
+ DataConnections.__ResetDependency__('activeProviders');
+ DataConnections.__ResetDependency__('providers');
+ });
+
+ it('should merge the the appropriate connect state UI and handler props based on the current provider connection state for a patient', () => {
+ const connectState1PatientProps = getDataConnectionProps(createPatientWithConnectionState('connectState1'), false, 'clinic125', setActiveHandlerStub).provider123;
+
+ expect(connectState1PatientProps.buttonDisabled).to.equal('connectState1 handler buttonDisabled stub');
+ expect(connectState1PatientProps.buttonHandler).to.be.a('function');
+ expect(connectState1PatientProps.buttonIcon).to.equal('connectState1 handler buttonIcon stub');
+ expect(connectState1PatientProps.buttonStyle).to.equal('connectState1 handler buttonStyle stub');
+ expect(connectState1PatientProps.buttonText).to.equal('connectState1 handler buttonText stub');
+ expect(connectState1PatientProps.icon).to.equal('connectState1 icon stub');
+ expect(connectState1PatientProps.iconLabel).to.equal('connection status: connectState1');
+ expect(connectState1PatientProps.label).to.equal('provider123 data connection state');
+ expect(connectState1PatientProps.logoImage).to.equal('provider123 logo image stub');
+ expect(connectState1PatientProps.logoImageLabel).to.equal('provider123 logo');
+ expect(connectState1PatientProps.messageColor).to.equal('#6D6D6D');
+ expect(connectState1PatientProps.messageText).to.equal('connectState1 message stub');
+ expect(connectState1PatientProps.providerName).to.equal('provider123');
+ expect(connectState1PatientProps.stateColor).to.equal('connectState1 color stub');
+ expect(connectState1PatientProps.stateText).to.equal('connectState1 text stub');
+
+ const connectState2PatientProps = getDataConnectionProps(createPatientWithConnectionState('connectState2'), false, 'clinic125', setActiveHandlerStub).provider123;
+
+ expect(connectState2PatientProps.buttonDisabled).to.equal('connectState2 handler buttonDisabled stub');
+ expect(connectState2PatientProps.buttonHandler).to.be.a('function');
+ expect(connectState2PatientProps.buttonIcon).to.equal('connectState2 handler buttonIcon stub');
+ expect(connectState2PatientProps.buttonStyle).to.equal('connectState2 handler buttonStyle stub');
+ expect(connectState2PatientProps.buttonText).to.equal('connectState2 handler buttonText stub');
+ expect(connectState2PatientProps.icon).to.equal('connectState2 icon stub');
+ expect(connectState2PatientProps.iconLabel).to.equal('connection status: connectState2');
+ expect(connectState2PatientProps.label).to.equal('provider123 data connection state');
+ expect(connectState2PatientProps.logoImage).to.equal('provider123 logo image stub');
+ expect(connectState2PatientProps.logoImageLabel).to.equal('provider123 logo');
+ expect(connectState2PatientProps.messageColor).to.equal('#6D6D6D');
+ expect(connectState2PatientProps.messageText).to.equal('connectState2 message stub');
+ expect(connectState2PatientProps.providerName).to.equal('provider123');
+ expect(connectState2PatientProps.stateColor).to.equal('connectState2 color stub');
+ expect(connectState2PatientProps.stateText).to.equal('connectState2 text stub');
+ });
+
+ it('should set the button handler to call the provided active handler setter with the appropriate args', () => {
+ const connectState1PatientProps = getDataConnectionProps(createPatientWithConnectionState('connectState1'), false, 'clinic125', setActiveHandlerStub).provider123;
+
+ expect(connectState1PatientProps.buttonHandler).to.be.a('function');
+ sinon.assert.notCalled(setActiveHandlerStub);
+
+ connectState1PatientProps.buttonHandler();
+ sinon.assert.calledWith(setActiveHandlerStub, {
+ action: 'connectState1 handler action stub',
+ args: 'connectState1 handler args stub',
+ emailRequired: 'connectState1 handler emailRequired stub',
+ patientUpdates: 'connectState1 handler patientUpdates stub',
+ providerName: 'provider123',
+ connectState: 'connectState1',
+ handler: 'connectState1 handler stub',
+ });
+ });
+});
+
+describe('DataConnections', () => {
+ let mount;
+
+ const api = {
+ clinics: {
+ getPatientFromClinic: sinon.stub(),
+ sendPatientDataProviderConnectRequest: sinon.stub(),
+ updateClinicPatient: sinon.stub(),
+ },
+ user: {
+ createRestrictedToken: sinon.stub().callsArgWith(1, null, { id: 'restrictedTokenID' }),
+ createOAuthProviderAuthorization: sinon.stub(),
+ deleteOAuthProviderAuthorization: sinon.stub(),
+ },
+ };
+
+ const getDateInPast = (amount, unit) => moment.utc().subtract(amount, unit).toISOString();
+
+ const patientWithState = (isClinicContext, state, opts = {}) => ({
+ id: 'patient123',
+ dataSources: state ? map(activeProviders, providerName => ({
+ providerName,
+ state,
+ createdTime: opts.createdTime,
+ modifiedTime: opts.modifiedTime,
+ expirationTime: opts.expirationTime,
+ lastImportTime: opts.lastImportTime,
+ latestDataTime: opts.latestDataTime,
+ })) : undefined,
+ connectionRequests: isClinicContext && opts.createdTime ? reduce(activeProviders, (res, providerName) => {
+ res[providerName] = [{ providerName, createdTime: opts.createdTime }];
+ return res;
+ }, {}) : undefined,
+ });
+
+ const clinicPatients = {
+ dataConnectionUnset: patientWithState(true),
+ dataConnectionInviteJustSent: patientWithState(true, 'pending', { createdTime: getDateInPast(5, 'seconds') }),
+ dataConnectionPending: patientWithState(true, 'pending', { createdTime: getDateInPast(5, 'days') }),
+ dataConnectionPendingReconnect: patientWithState(true, 'pendingReconnect', { createdTime: getDateInPast(10, 'days') }),
+ dataConnectionPendingExpired: patientWithState(true, 'pending', { createdTime: getDateInPast(31, 'days'), expirationTime: getDateInPast(1, 'days') }),
+ dataConnectionConnected: patientWithState(true, 'connected'),
+ dataConnectionDisconnected: patientWithState(true, 'disconnected', { modifiedTime: getDateInPast(7, 'hours') }),
+ dataConnectionError: patientWithState(true, 'error', { modifiedTime: getDateInPast(20, 'minutes') }),
+ dataConnectionUnknown: patientWithState(true, 'foo'),
+ }
+
+ const userPatients = {
+ dataConnectionUnset: patientWithState(false),
+ dataConnectionJustConnected: patientWithState(false, 'connected', { createdTime: getDateInPast(1, 'minutes') }),
+ dataConnectionConnectedWithNoData: patientWithState(false, 'connected', { lastImportTime: getDateInPast(5, 'minutes') }),
+ dataConnectionConnectedWithData: patientWithState(false, 'connected', { lastImportTime: getDateInPast(1, 'minutes'), latestDataTime: getDateInPast(35, 'minutes') }),
+ dataConnectionDisconnected: patientWithState(false, 'disconnected', { modifiedTime: getDateInPast(1, 'hour') }),
+ dataConnectionError: patientWithState(false, 'error', { modifiedTime: getDateInPast(6, 'days') }),
+ dataConnectionUnknown: patientWithState(false, 'foo'),
+ }
+
+ let defaultProps = {
+ trackMetric: sinon.stub(),
+ };
+
+ before(() => {
+ mount = createMount();
+ });
+
+ after(() => {
+ mount.cleanUp();
+ });
+
+ const defaultWorkingState = {
+ inProgress: false,
+ completed: false,
+ notification: null,
+ };
+
+ const clinicianUserLoggedInState = {
+ blip: {
+ timePrefs: { timezoneName: 'US/Eastern' },
+ working: {
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
+ updatingClinicPatient: defaultWorkingState,
+ },
+ selectedClinicId: 'clinicID123',
+ loggedInUserId: 'clinician123',
+ },
+ };
+
+ const patientUserLoggedInState = {
+ blip: {
+ working: {
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
+ updatingClinicPatient: defaultWorkingState,
+ },
+ selectedClinicId: 'clinicID123',
+ loggedInUserId: 'patient123',
+ },
+ };
+
+ let wrapper;
+ const mountWrapper = (store, patient) => {
+ wrapper = mount(
+
+
+
+
+
+ );
+ };
+
+ beforeEach(() => {
+ DataConnections.__Rewire__('api', api);
+ PatientEmailModal.__Rewire__('api', api);
+ });
+
+ afterEach(() => {
+ defaultProps.trackMetric.resetHistory();
+ api.clinics.getPatientFromClinic.resetHistory();
+ api.clinics.sendPatientDataProviderConnectRequest.resetHistory();
+ api.clinics.updateClinicPatient.resetHistory();
+ api.user.createRestrictedToken.resetHistory();
+ api.user.createOAuthProviderAuthorization.resetHistory();
+ api.user.deleteOAuthProviderAuthorization.resetHistory();
+ DataConnections.__ResetDependency__('api');
+ PatientEmailModal.__ResetDependency__('api');
+ });
+
+ context('clinic patients', () => {
+ describe('data connection unset', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionUnset);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('No Pending Connections');
+ expect(dexcomConnection.find('.state-message')).to.have.lengthOf(0);
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('No Pending Connections');
+ expect(abbottConnection.find('.state-message')).to.have.lengthOf(0);
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionUnset);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Email Invite');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Email Invite');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.clinics.updateClinicPatient, 'clinicID123', 'patient123', sinon.match({ dataSources: [ { providerName: 'dexcom', state: 'pending' } ] }));
+ sinon.assert.calledWith(api.clinics.updateClinicPatient, 'clinicID123', 'patient123', sinon.match({ dataSources: [ { providerName: 'abbott', state: 'pending' } ] }));
+ done();
+ })
+ });
+ });
+
+ describe('data connection invite just sent', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionInviteJustSent);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Connection Pending');
+ expect(dexcomConnection.find('.state-message')).to.have.lengthOf(0);
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Connection Pending');
+ expect(abbottConnection.find('.state-message')).to.have.lengthOf(0);
+ });
+
+ it('should render a disabled action buttons with appropriate text', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionInviteJustSent);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.props().disabled).to.be.true;
+ expect(dexcomActionButton.text()).to.equal('Invite Sent');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.props().disabled).to.be.true;
+ expect(abbottActionButton.text()).to.equal('Invite Sent');
+ });
+ });
+
+ describe('data connection pending', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionPending);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Connection Pending');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Invite sent 5 days ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Connection Pending');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Invite sent 5 days ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when confirmed in dialog', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionPending);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Resend Invite');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Resend Invite');
+
+ // Open and submit the dexcom resend invite confirmation modal
+ const resendDialog = () => wrapper.find('#resendDataSourceConnectRequest').at(1);
+ expect(resendDialog().props().open).to.be.false;
+ dexcomActionButton.simulate('click');
+ expect(resendDialog().props().open).to.be.true;
+
+ const resendInvite = resendDialog().find(Button).filter({variant: 'primary'});
+ expect(resendInvite).to.have.length(1);
+
+ const expectedActions = [
+ {
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST',
+ },
+ ];
+
+ store.clearActions();
+ resendInvite.props().onClick();
+ expect(store.getActions()).to.eql(expectedActions);
+ sinon.assert.calledWith(
+ api.clinics.sendPatientDataProviderConnectRequest,
+ 'clinicID123',
+ 'patient123',
+ 'dexcom',
+ );
+ });
+ });
+
+ describe('data reconnection pending', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionPendingReconnect);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Invite Sent');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Invite sent 10 days ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Invite Sent');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Invite sent 10 days ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when confirmed in dialog', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionPendingReconnect);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Resend Invite');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Resend Invite');
+
+ // Open and submit the dexcom resend invite confirmation modal
+ const resendDialog = () => wrapper.find('#resendDataSourceConnectRequest').at(1);
+ expect(resendDialog().props().open).to.be.false;
+ dexcomActionButton.simulate('click');
+ expect(resendDialog().props().open).to.be.true;
+
+ const resendInvite = resendDialog().find(Button).filter({variant: 'primary'});
+ expect(resendInvite).to.have.length(1);
+
+ const expectedActions = [
+ {
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST',
+ },
+ ];
+
+ store.clearActions();
+ resendInvite.props().onClick();
+ expect(store.getActions()).to.eql(expectedActions);
+ sinon.assert.calledWith(
+ api.clinics.sendPatientDataProviderConnectRequest,
+ 'clinicID123',
+ 'patient123',
+ 'dexcom',
+ );
+ });
+ });
+
+ describe('data connection pending expired', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionPendingExpired);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Invite Expired');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Sent over one month ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Invite Expired');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Sent over one month ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when confirmed in dialog', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionPendingExpired);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Resend Invite');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Resend Invite');
+
+ // Open and submit the dexcom resend invite confirmation modal
+ const resendDialog = () => wrapper.find('#resendDataSourceConnectRequest').at(1);
+ expect(resendDialog().props().open).to.be.false;
+ dexcomActionButton.simulate('click');
+ expect(resendDialog().props().open).to.be.true;
+
+ const resendInvite = resendDialog().find(Button).filter({variant: 'primary'});
+ expect(resendInvite).to.have.length(1);
+
+ const expectedActions = [
+ {
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST',
+ },
+ ];
+
+ store.clearActions();
+ resendInvite.props().onClick();
+ expect(store.getActions()).to.eql(expectedActions);
+ sinon.assert.calledWith(
+ api.clinics.sendPatientDataProviderConnectRequest,
+ 'clinicID123',
+ 'patient123',
+ 'dexcom',
+ );
+ });
+ });
+
+ describe('data connected', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionConnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Connected');
+ expect(dexcomConnection.find('.state-message')).to.have.lengthOf(0);
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Connected');
+ expect(abbottConnection.find('.state-message')).to.have.lengthOf(0);
+ });
+
+ it('should not render an action button', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionConnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(0);
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(0);
+ });
+ });
+
+ describe('data connection disconnected', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionDisconnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Patient Disconnected');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Last update 7 hours ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Patient Disconnected');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Last update 7 hours ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when confirmed in dialog', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionDisconnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Resend Invite');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Resend Invite');
+
+ // Open and submit the dexcom resend invite confirmation modal
+ const resendDialog = () => wrapper.find('#resendDataSourceConnectRequest').at(1);
+ expect(resendDialog().props().open).to.be.false;
+ dexcomActionButton.simulate('click');
+ expect(resendDialog().props().open).to.be.true;
+
+ const resendInvite = resendDialog().find(Button).filter({variant: 'primary'});
+ expect(resendInvite).to.have.length(1);
+
+ const expectedActions = [
+ {
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST',
+ },
+ ];
+
+ store.clearActions();
+ resendInvite.props().onClick();
+ expect(store.getActions()).to.eql(expectedActions);
+ sinon.assert.calledWith(
+ api.clinics.sendPatientDataProviderConnectRequest,
+ 'clinicID123',
+ 'patient123',
+ 'dexcom',
+ );
+ });
+ });
+
+ describe('data connection error', () => {
+ it('should render all appropriate data connection statuses and messages', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionError);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Error Connecting');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Last update 20 minutes ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Error Connecting');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Last update 20 minutes ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when confirmed in dialog', () => {
+ const store = mockStore(clinicianUserLoggedInState);
+ mountWrapper(store, clinicPatients.dataConnectionError);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Resend Invite');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Resend Invite');
+
+ // Open and submit the dexcom resend invite confirmation modal
+ const resendDialog = () => wrapper.find('#resendDataSourceConnectRequest').at(1);
+ expect(resendDialog().props().open).to.be.false;
+ dexcomActionButton.simulate('click');
+ expect(resendDialog().props().open).to.be.true;
+
+ const resendInvite = resendDialog().find(Button).filter({variant: 'primary'});
+ expect(resendInvite).to.have.length(1);
+
+ const expectedActions = [
+ {
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST',
+ },
+ ];
+
+ store.clearActions();
+ resendInvite.props().onClick();
+ expect(store.getActions()).to.eql(expectedActions);
+ sinon.assert.calledWith(
+ api.clinics.sendPatientDataProviderConnectRequest,
+ 'clinicID123',
+ 'patient123',
+ 'dexcom',
+ );
+ });
+ });
+ });
+
+ context('logged-in user patients', () => {
+ describe('data connection unset', () => {
+ it('should not render data connection status or message', () => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionUnset);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text')).to.have.lengthOf(0);
+ expect(dexcomConnection.find('.state-message')).to.have.lengthOf(0);
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text')).to.have.lengthOf(0);
+ expect(abbottConnection.find('.state-message')).to.have.lengthOf(0);
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionUnset);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Connect');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Connect');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.user.createRestrictedToken, sinon.match({ paths: [ '/v1/oauth/dexcom' ] }));
+ sinon.assert.calledWith(api.user.createOAuthProviderAuthorization, 'dexcom', 'restrictedTokenID');
+
+ sinon.assert.calledWith(api.user.createRestrictedToken, sinon.match({ paths: [ '/v1/oauth/abbott' ] }));
+ sinon.assert.calledWith(api.user.createOAuthProviderAuthorization, 'abbott', 'restrictedTokenID');
+ done();
+ });
+ });
+ });
+
+ describe('data connection just connected', () => {
+ it('should render data connection status and message', () => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionJustConnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Connecting');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - This can take a few minutes');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Connecting');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - This can take a few minutes');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionJustConnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Disconnect');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Disconnect');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.user.deleteOAuthProviderAuthorization, 'dexcom');
+ sinon.assert.calledWith(api.user.deleteOAuthProviderAuthorization, 'abbott');
+ done();
+ });
+ });
+ });
+
+ describe('data connection connected with no data', () => {
+ it('should render data connection status and message', () => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionConnectedWithNoData);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Connected');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - No data found as of 5 minutes ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Connected');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - No data found as of 5 minutes ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionConnectedWithNoData);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Disconnect');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Disconnect');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.user.deleteOAuthProviderAuthorization, 'dexcom');
+ sinon.assert.calledWith(api.user.deleteOAuthProviderAuthorization, 'abbott');
+ done();
+ });
+ });
+ });
+
+ describe('data connection connected with data', () => {
+ it('should render data connection status and message', () => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionConnectedWithData);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Connected');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Last data 35 minutes ago');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Connected');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Last data 35 minutes ago');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionConnectedWithData);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Disconnect');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Disconnect');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.user.deleteOAuthProviderAuthorization, 'dexcom');
+ sinon.assert.calledWith(api.user.deleteOAuthProviderAuthorization, 'abbott');
+ done();
+ });
+ });
+ });
+
+ describe('data connection disconnected', () => {
+ it('should not render data connection status or message', () => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionDisconnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text')).to.have.lengthOf(0);
+ expect(dexcomConnection.find('.state-message')).to.have.lengthOf(0);
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text')).to.have.lengthOf(0);
+ expect(abbottConnection.find('.state-message')).to.have.lengthOf(0);
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionDisconnected);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Connect');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Connect');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.user.createRestrictedToken, sinon.match({ paths: [ '/v1/oauth/dexcom' ] }));
+ sinon.assert.calledWith(api.user.createOAuthProviderAuthorization, 'dexcom', 'restrictedTokenID');
+
+ sinon.assert.calledWith(api.user.createRestrictedToken, sinon.match({ paths: [ '/v1/oauth/abbott' ] }));
+ sinon.assert.calledWith(api.user.createOAuthProviderAuthorization, 'abbott', 'restrictedTokenID');
+ done();
+ });
+ });
+ });
+
+ describe('data connection error', () => {
+ it('should render data connection status and message', () => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionError);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ expect(dexcomConnection.find('.state-text').hostNodes().text()).to.equal('Error Connecting');
+ expect(dexcomConnection.find('.state-message').hostNodes().text()).to.equal(' - Last update 6 days ago. Please reconnect your account to keep syncing data.');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ expect(abbottConnection.find('.state-text').hostNodes().text()).to.equal('Error Connecting');
+ expect(abbottConnection.find('.state-message').hostNodes().text()).to.equal(' - Last update 6 days ago. Please reconnect your account to keep syncing data.');
+ });
+
+ it('should render appropriate buttons and dispatch appropriate actions when clicked', done => {
+ const store = mockStore(patientUserLoggedInState);
+ mountWrapper(store, userPatients.dataConnectionError);
+
+ const connections = wrapper.find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ const dexcomConnection = wrapper.find('#data-connection-dexcom').hostNodes();
+ expect(dexcomConnection).to.have.lengthOf(1);
+ const dexcomActionButton = dexcomConnection.find('.action').hostNodes();
+ expect(dexcomActionButton).to.have.lengthOf(1);
+ expect(dexcomActionButton.text()).to.equal('Reconnect');
+
+ const abbottConnection = wrapper.find('#data-connection-abbott').hostNodes();
+ expect(abbottConnection).to.have.lengthOf(1);
+ const abbottActionButton = abbottConnection.find('.action').hostNodes();
+ expect(abbottActionButton).to.have.lengthOf(1);
+ expect(abbottActionButton.text()).to.equal('Reconnect');
+
+ store.clearActions();
+ dexcomActionButton.simulate('click');
+ abbottActionButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledWith(api.user.createRestrictedToken, sinon.match({ paths: [ '/v1/oauth/dexcom' ] }));
+ sinon.assert.calledWith(api.user.createOAuthProviderAuthorization, 'dexcom', 'restrictedTokenID');
+
+ sinon.assert.calledWith(api.user.createRestrictedToken, sinon.match({ paths: [ '/v1/oauth/abbott' ] }));
+ sinon.assert.calledWith(api.user.createOAuthProviderAuthorization, 'abbott', 'restrictedTokenID');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/test/unit/components/datasources/DataConnectionsModal.test.js b/test/unit/components/datasources/DataConnectionsModal.test.js
new file mode 100644
index 0000000000..09b044178a
--- /dev/null
+++ b/test/unit/components/datasources/DataConnectionsModal.test.js
@@ -0,0 +1,206 @@
+import React from 'react';
+import { createMount } from '@material-ui/core/test-utils';
+import configureStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+import DataConnectionsModal from '../../../../app/components/datasources/DataConnectionsModal';
+import DataConnections from '../../../../app/components/datasources/DataConnections';
+import { Dialog } from '../../../../app/components/elements/Dialog';
+import { ToastProvider } from '../../../../app/providers/ToastProvider';
+
+/* global chai */
+/* global sinon */
+/* global describe */
+/* global context */
+/* global it */
+/* global beforeEach */
+/* global before */
+/* global afterEach */
+/* global after */
+
+const expect = chai.expect;
+const mockStore = configureStore([thunk]);
+
+describe('DataConnectionsModal', () => {
+ let mount;
+
+ const api = {
+ clinics: {
+ getPatientFromClinic: sinon.stub(),
+ updateClinicPatient: sinon.stub(),
+ },
+ };
+
+ let wrapper;
+ let formikContext;
+
+ const patientWithEmail = {
+ id: 'patient123',
+ fullName: 'Patient 123',
+ birthDate: '2004-02-03',
+ mrn: '12345',
+ email: 'patient@test.ca',
+ tags: ['tag1', 'tag2'],
+ permissions: { custodian: {} },
+ };
+
+ const patientWithoutEmail = {
+ id: 'patient123',
+ fullName: 'Patient 123',
+ birthDate: '2004-02-03',
+ mrn: '12345',
+ tags: ['tag1', 'tag2'],
+ permissions: { custodian: {} },
+ };
+
+ const patientWithoutCustodialPermission = {
+ id: 'patient123',
+ fullName: 'Patient 123',
+ birthDate: '2004-02-03',
+ mrn: '12345',
+ tags: ['tag1', 'tag2'],
+ permissions: {},
+ };
+
+ let defaultProps = {
+ open: true,
+ onClose: sinon.stub(),
+ onBack: sinon.stub(),
+ patient: patientWithEmail,
+ trackMetric: sinon.stub(),
+ };
+
+ before(() => {
+ mount = createMount();
+ });
+
+ after(() => {
+ mount.cleanUp();
+ });
+
+ const defaultWorkingState = {
+ inProgress: false,
+ completed: false,
+ notification: null,
+ };
+
+ const fetchedDataState = {
+ blip: {
+ working: {
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
+ updatingClinicPatient: defaultWorkingState,
+ },
+ clinics: {
+ clinicID123: {
+ mrnSettings: {
+ required: true,
+ },
+ patientTags: [
+ { id: 'tag1' },
+ { id: 'tag2' },
+ ],
+ },
+ },
+ selectedClinicId: 'clinicID123',
+ },
+ };
+
+ let store = mockStore(fetchedDataState);
+
+ beforeEach(() => {
+ DataConnectionsModal.__Rewire__('api', api);
+ DataConnections.__Rewire__('api', api);
+
+ wrapper = mount(
+
+
+
+
+
+ );
+ });
+
+ afterEach(() => {
+ defaultProps.trackMetric.resetHistory();
+ defaultProps.onClose.resetHistory();
+ defaultProps.onBack.resetHistory();
+ DataConnections.__ResetDependency__('api');
+ DataConnectionsModal.__ResetDependency__('api');
+ });
+
+ it('should render the modal title', () => {
+ const dialog = () => wrapper.find(Dialog).at(0);
+ expect(dialog()).to.have.lengthOf(1);
+ expect(dialog().props().open).to.be.true;
+
+ const title = dialog().find('#data-connections-title').hostNodes();
+ expect(title).to.have.lengthOf(1);
+ expect(title.text()).to.equal('Bring Data into Tidepool');
+ });
+
+ it('should render patient details', () => {
+ const dialog = () => wrapper.find(Dialog).at(0);
+ expect(dialog()).to.have.lengthOf(1);
+ expect(dialog().props().open).to.be.true;
+
+ const details = dialog().find('#data-connections-patient-details').hostNodes();
+ expect(details).to.have.lengthOf(1);
+ });
+
+ it('should render a patients data connection statuses', () => {
+ const dialog = () => wrapper.find(Dialog).at(0);
+ expect(dialog()).to.have.lengthOf(1);
+ expect(dialog().props().open).to.be.true;
+
+ const connections = dialog().find('.data-connection').hostNodes();
+ expect(connections).to.have.lengthOf(2);
+
+ expect(connections.at(0).is('#data-connection-dexcom')).to.be.true;
+ expect(connections.at(1).is('#data-connection-abbott')).to.be.true;
+ });
+
+ it('should allow opening a dialog for updating an existing email address for a custodial patient', () => {
+ const dialog = () => wrapper.find('Dialog#patient-email-modal');
+ expect(dialog()).to.have.lengthOf(0);
+
+ const dialogButton = wrapper.find('#data-connections-open-email-modal').hostNodes();
+ expect(dialogButton).to.have.lengthOf(1);
+ expect(dialogButton.text()).to.equal(patientWithEmail.email);
+
+ dialogButton.simulate('click');
+ expect(dialog()).to.have.lengthOf(1);
+ expect(dialog().props().open).to.be.true;
+ });
+
+ it('should not allow opening a dialog for updating an email address for a custodial patient without an existing email address', () => {
+ wrapper = mount(
+
+
+
+
+
+ );
+
+ const dialog = () => wrapper.find('Dialog#patient-email-modal');
+ expect(dialog()).to.have.lengthOf(0);
+
+ const dialogButton = wrapper.find('#data-connections-open-email-modal').hostNodes();
+ expect(dialogButton).to.have.lengthOf(0);
+ });
+
+ it('should not allow opening a dialog for updating an existing email address for a patient without custodial access', () => {
+ wrapper = mount(
+
+
+
+
+
+ );
+
+ const dialog = () => wrapper.find('Dialog#patient-email-modal');
+ expect(dialog()).to.have.lengthOf(0);
+
+ const dialogButton = wrapper.find('#data-connections-open-email-modal').hostNodes();
+ expect(dialogButton).to.have.lengthOf(0);
+ });
+});
diff --git a/test/unit/components/datasources/PatientDetails.test.js b/test/unit/components/datasources/PatientDetails.test.js
new file mode 100644
index 0000000000..5009879db2
--- /dev/null
+++ b/test/unit/components/datasources/PatientDetails.test.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import { createMount } from '@material-ui/core/test-utils';
+import PatientDetails from '../../../../app/components/datasources/PatientDetails';
+
+/* global chai */
+/* global describe */
+/* global it */
+/* global beforeEach */
+
+const expect = chai.expect;
+
+describe('PatientDetails', () => {
+ const mount = createMount();
+
+ let wrapper;
+ const defaultProps = {
+ patient: {
+ id: 'patient123',
+ fullName: 'Patient 123',
+ birthDate: '2004-02-03',
+ mrn: '12345',
+ },
+ };
+
+ const noPatientMRNProps = {
+ patient: {
+ id: 'patient123',
+ fullName: 'Patient 123',
+ birthDate: '2004-02-03',
+ },
+ };
+
+ beforeEach(() => {
+ wrapper = mount();
+ });
+
+ it('should render the provided patient details', () => {
+ const fullName = wrapper.find('#patient-details-fullName').hostNodes();
+ expect(fullName).to.have.lengthOf(1);
+ expect(fullName.text()).to.equal('Patient 123');
+
+ const birthDate = wrapper.find('#patient-details-birthDate').hostNodes();
+ expect(birthDate).to.have.lengthOf(1);
+ expect(birthDate.text()).to.equal('DOB: 2004-02-03');
+
+ const mrn = wrapper.find('#patient-details-mrn').hostNodes();
+ expect(mrn).to.have.lengthOf(1);
+ expect(mrn.text()).to.equal('MRN: 12345');
+ });
+
+ it('should render the provided patient details with mrn missing', () => {
+ wrapper = mount();
+
+ const fullName = wrapper.find('#patient-details-fullName').hostNodes();
+ expect(fullName).to.have.lengthOf(1);
+ expect(fullName.text()).to.equal('Patient 123');
+
+ const birthDate = wrapper.find('#patient-details-birthDate').hostNodes();
+ expect(birthDate).to.have.lengthOf(1);
+ expect(birthDate.text()).to.equal('DOB: 2004-02-03');
+
+ const mrn = wrapper.find('#patient-details-mrn').hostNodes();
+ expect(mrn).to.have.lengthOf(1);
+ expect(mrn.text()).to.equal('MRN: -');
+ });
+});
diff --git a/test/unit/components/datasources/PatientEmailModal.test.js b/test/unit/components/datasources/PatientEmailModal.test.js
new file mode 100644
index 0000000000..69d7a21fb0
--- /dev/null
+++ b/test/unit/components/datasources/PatientEmailModal.test.js
@@ -0,0 +1,173 @@
+import React from 'react';
+import { createMount } from '@material-ui/core/test-utils';
+import configureStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+import PatientEmailModal from '../../../../app/components/datasources/PatientEmailModal';
+import Banner from '../../../../app/components/elements/Banner';
+import { Dialog } from '../../../../app/components/elements/Dialog';
+import noop from 'lodash/noop';
+
+/* global chai */
+/* global sinon */
+/* global describe */
+/* global context */
+/* global it */
+/* global beforeEach */
+/* global before */
+/* global afterEach */
+/* global after */
+
+const expect = chai.expect;
+const mockStore = configureStore([thunk]);
+
+describe('PatientEmailModal', () => {
+ let mount;
+
+ const api = {
+ clinics: {
+ updateClinicPatient: sinon.stub(),
+ },
+ };
+
+ let wrapper;
+ const patientWithEmail = {
+ id: 'patient123',
+ fullName: 'Patient 123',
+ birthDate: '2004-02-03',
+ mrn: '12345',
+ email: 'patient@test.ca',
+ tags: ['tag1', 'tag2']
+ };
+
+ let defaultProps = {
+ open: true,
+ onClose: sinon.stub(),
+ onFormChange: sinon.stub(),
+ onSubmit: sinon.stub(),
+ processing: false,
+ patient: patientWithEmail,
+ trackMetric: sinon.stub(),
+ };
+
+ before(() => {
+ mount = createMount();
+ });
+
+ after(() => {
+ mount.cleanUp();
+ });
+
+ const fetchedDataState = {
+ blip: {
+ clinics: {
+ clinicID123: {
+ mrnSettings: {
+ required: true,
+ },
+ patientTags: [
+ { id: 'tag1' },
+ { id: 'tag2' },
+ ],
+ },
+ },
+ selectedClinicId: 'clinicID123',
+ },
+ };
+
+ let store = mockStore(fetchedDataState);
+
+ beforeEach(() => {
+ PatientEmailModal.__Rewire__('api', api);
+ wrapper = mount(
+
+
+
+ );
+ });
+
+ afterEach(() => {
+ defaultProps.trackMetric.resetHistory();
+ defaultProps.onFormChange.resetHistory();
+ defaultProps.onSubmit.resetHistory();
+ PatientEmailModal.__ResetDependency__('api');
+ });
+
+ it('should render a dialog for updating an existing patient email address', done => {
+ const dialog = () => wrapper.find(Dialog).at(0);
+ expect(dialog()).to.have.lengthOf(1);
+ expect(dialog().props().open).to.be.true;
+
+ const title = dialog().find('#patient-email-modal-title').hostNodes();
+ expect(title.text()).to.equal('Edit Patient Email');
+
+ const submitButton = dialog().find('Button#patient-email-modal-submit');
+ expect(submitButton.text()).to.equal('Save');
+
+ const banner = dialog().find(Banner);
+ expect(banner.find('.title').hostNodes().text()).to.equal('Changing this email will update the email associated with the account.')
+
+ expect(dialog().find('input[name="email"]').prop('value')).to.equal('patient@test.ca');
+ dialog().find('input[name="email"]').simulate('change', { persist: noop, target: { name: 'email', value: 'patient-updated@test.ca' } });
+ expect(dialog().find('input[name="email"]').prop('value')).to.equal('patient-updated@test.ca');
+
+ sinon.assert.calledWith(defaultProps.onFormChange, sinon.match({ values: {
+ birthDate: '2004-02-03',
+ email: 'patient-updated@test.ca',
+ fullName: 'Patient 123',
+ mrn: '12345',
+ tags: ['tag1', 'tag2'],
+ dataSources: [] ,
+ } }));
+
+ store.clearActions();
+ submitButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledOnce(defaultProps.onSubmit);
+ done();
+ });
+ });
+
+ it('should render a dialog for adding an email and sending an invite for a patient without an email address', done => {
+ wrapper = mount(
+
+
+
+ );
+
+ const dialog = () => wrapper.find(Dialog).at(0);
+ expect(dialog()).to.have.lengthOf(1);
+ expect(dialog().props().open).to.be.true;
+
+ const title = dialog().find('#patient-email-modal-title').hostNodes();
+ expect(title.text()).to.equal('Add a Patient Email');
+
+ const submitButton = dialog().find('Button#patient-email-modal-submit');
+ expect(submitButton.text()).to.equal('Send Invite');
+
+ const banner = dialog().find(Banner);
+ expect(banner.find('.title').hostNodes().text()).to.equal('Please set the email address for this patient account.')
+
+ expect(dialog().find('input[name="email"]').prop('value')).to.equal('');
+ dialog().find('input[name="email"]').simulate('change', { persist: noop, target: { name: 'email', value: 'patient@test.ca' } });
+ expect(dialog().find('input[name="email"]').prop('value')).to.equal('patient@test.ca');
+
+ sinon.assert.calledWith(defaultProps.onFormChange, sinon.match({ values: {
+ birthDate: '2004-02-03',
+ email: 'patient@test.ca',
+ fullName: 'Patient 123',
+ mrn: '12345',
+ tags: ['tag1', 'tag2'],
+ dataSources: [] ,
+ } }));
+
+ store.clearActions();
+ submitButton.simulate('click');
+
+ setTimeout(() => {
+ sinon.assert.calledOnce(defaultProps.onSubmit);
+ done();
+ });
+ });
+});
diff --git a/test/unit/components/dexcombanner.test.js b/test/unit/components/dexcombanner.test.js
index b55c44ea37..09ba21c40c 100644
--- a/test/unit/components/dexcombanner.test.js
+++ b/test/unit/components/dexcombanner.test.js
@@ -31,6 +31,7 @@ import thunk from 'redux-thunk';
import { DexcomBanner } from '../../../app/components/dexcombanner';
import { ToastProvider } from '../../../app/providers/ToastProvider';
import Button from '../../../app/components/elements/Button';
+const async = require('../../../app/redux/actions/async');
const expect = chai.expect;
const mockStore = configureStore([thunk]);
@@ -44,7 +45,7 @@ describe('DexcomBanner', () => {
push: sinon.stub(),
api: {
clinics: {
- sendPatientDexcomConnectRequest: sinon.stub().callsArgWith(2, null, { lastRequestedDexcomConnectTime: '2022-02-02T00:00:00.000Z'}),
+ sendPatientDataProviderConnectRequest: sinon.stub().callsArgWith(3, null),
},
},
};
@@ -58,7 +59,7 @@ describe('DexcomBanner', () => {
const blipState = {
blip: {
working: {
- sendingPatientDexcomConnectRequest: defaultWorkingState,
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
},
timePrefs: {
timezoneName: 'UTC'
@@ -71,7 +72,7 @@ describe('DexcomBanner', () => {
email: 'patient1@test.ca',
fullName: 'patient1',
birthDate: '1999-01-01',
- lastRequestedDexcomConnectTime: '2021-10-19T16:27:59.504Z',
+ connectionRequests: { dexcom: [{ providerName: 'dexcom', createdTime: '2021-10-19T16:27:59.504Z' }] },
dataSources: [
{ providerName: 'dexcom', state: 'error' },
],
@@ -95,12 +96,16 @@ describe('DexcomBanner', () => {
beforeEach(() => {
wrapper = createWrapper(props);
+ async.__Rewire__('moment', {
+ utc: () => ({ toISOString: () => '2022-02-02T00:00:00.000Z'})
+ });
});
afterEach(() => {
props.onClose.reset();
props.onClick.reset();
props.trackMetric.reset();
+ async.__ResetDependency__('moment');
});
it('should render without errors when provided all required props', () => {
@@ -282,7 +287,7 @@ describe('DexcomBanner', () => {
it('should open a confirmation dialog to resend the reconnection email', () => {
const resendButton = () => wrapper.find('.dexcomBanner-action button');
- const resendDialog = () => wrapper.find('#resendDexcomConnectRequest').at(1);
+ const resendDialog = () => wrapper.find('#resendDataSourceConnectRequest').at(1);
expect(resendDialog().props().open).to.be.false;
resendButton().simulate('click');
expect(resendDialog().props().open).to.be.true;
@@ -294,14 +299,15 @@ describe('DexcomBanner', () => {
const expectedActions = [
{
- type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST',
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST',
},
{
- type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS',
+ type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS',
payload: {
clinicId: 'clinicID123',
- lastRequestedDexcomConnectTime: '2022-02-02T00:00:00.000Z',
+ createdTime: '2022-02-02T00:00:00.000Z',
patientId: 'clinicPatient123',
+ providerName: 'dexcom',
},
},
];
@@ -310,9 +316,10 @@ describe('DexcomBanner', () => {
resendInvite.props().onClick();
expect(store.getActions()).to.eql(expectedActions);
sinon.assert.calledWith(
- props.api.clinics.sendPatientDexcomConnectRequest,
+ props.api.clinics.sendPatientDataProviderConnectRequest,
'clinicID123',
- 'clinicPatient123'
+ 'clinicPatient123',
+ 'dexcom',
);
});
diff --git a/test/unit/pages/ClinicPatients.test.js b/test/unit/pages/ClinicPatients.test.js
index 167b543b44..f1da9f3d2f 100644
--- a/test/unit/pages/ClinicPatients.test.js
+++ b/test/unit/pages/ClinicPatients.test.js
@@ -17,6 +17,8 @@ import { URL_TIDEPOOL_PLUS_PLANS } from '../../../app/core/constants';
import Button from '../../../app/components/elements/Button';
import TideDashboardConfigForm from '../../../app/components/clinic/TideDashboardConfigForm';
import RpmReportConfigForm from '../../../app/components/clinic/RpmReportConfigForm';
+import DataConnectionsModal from '../../../app/components/datasources/DataConnectionsModal';
+import DataConnections from '../../../app/components/datasources/DataConnections';
import mockRpmReportPatients from '../../fixtures/mockRpmReportPatients.json'
import LDClientMock from '../../fixtures/LDClientMock';
@@ -53,7 +55,7 @@ describe('ClinicPatients', () => {
createClinicCustodialAccount: sinon.stub().callsArgWith(2, null, { id: 'stubbedId' }),
updateClinicPatient: sinon.stub().callsArgWith(3, null, { id: 'stubbedId', stubbedUpdates: 'foo' }),
sendPatientUploadReminder: sinon.stub().callsArgWith(2, null, { lastUploadReminderTime: '2022-02-02T00:00:00.000Z'}),
- sendPatientDexcomConnectRequest: sinon.stub().callsArgWith(2, null, { lastRequestedDexcomConnectTime: '2022-02-02T00:00:00.000Z'}),
+ sendPatientDataProviderConnectRequest: sinon.stub().callsArgWith(2, null),
createClinicPatientTag: sinon.stub(),
updateClinicPatientTag: sinon.stub(),
deleteClinicPatientTag: sinon.stub(),
@@ -77,14 +79,18 @@ describe('ClinicPatients', () => {
defaultProps.api.clinics.getPatientsForClinic.resetHistory();
defaultProps.api.clinics.deletePatientFromClinic.resetHistory();
defaultProps.api.clinics.createClinicCustodialAccount.resetHistory();
- defaultProps.api.clinics.sendPatientDexcomConnectRequest.resetHistory();
+ defaultProps.api.clinics.sendPatientDataProviderConnectRequest.resetHistory();
defaultProps.api.clinics.updateClinicPatient.resetHistory();
defaultProps.api.clinics.getPatientsForRpmReport.resetHistory();
ClinicPatients.__Rewire__('useLDClient', sinon.stub().returns(new LDClientMock()));
+ DataConnections.__Rewire__('api', defaultProps.api);
+ DataConnectionsModal.__Rewire__('api', defaultProps.api);
});
afterEach(() => {
ClinicPatients.__ResetDependency__('useLDClient');
+ DataConnections.__ResetDependency__('api');
+ DataConnectionsModal.__ResetDependency__('api');
});
after(() => {
@@ -140,7 +146,7 @@ describe('ClinicPatients', () => {
updatingClinicPatient: defaultWorkingState,
creatingClinicCustodialAccount: defaultWorkingState,
sendingPatientUploadReminder: defaultWorkingState,
- sendingPatientDexcomConnectRequest: defaultWorkingState,
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
creatingClinicPatientTag: defaultWorkingState,
updatingClinicPatientTag: defaultWorkingState,
deletingClinicPatientTag: defaultWorkingState,
@@ -215,7 +221,7 @@ describe('ClinicPatients', () => {
email: 'patient1@test.ca',
fullName: 'patient1',
birthDate: '1999-01-01',
- lastRequestedDexcomConnectTime: '2021-10-19T16:27:59.504Z',
+ createdTime: '2021-10-19T16:27:59.504Z',
dataSources: [
{ providerName: 'dexcom', state: 'pending' },
],
@@ -234,7 +240,7 @@ describe('ClinicPatients', () => {
email: 'patient3@test.ca',
fullName: 'patient3',
birthDate: '1999-01-01',
- lastRequestedDexcomConnectTime: '2021-10-19T16:27:59.504Z',
+ createdTime: '2021-10-19T16:27:59.504Z',
dataSources: [
{ providerName: 'dexcom', state: 'disconnected' },
],
@@ -572,11 +578,11 @@ describe('ClinicPatients', () => {
context('patients hidden', () => {
beforeEach(() => {
- const initialState = {
- blip: {
+ const initialState = {
+ blip: {
...hasPatientsState.blip,
patientListFilters: { isPatientListVisible: false, patientListSearchTextInput: '' }
- }
+ }
}
store = mockStore(initialState);
@@ -592,7 +598,7 @@ describe('ClinicPatients', () => {
defaultProps.trackMetric.resetHistory();
});
- it('should render a button that toggles patients to be visible', () => {
+ it('should render a button that toggles patients to be visible', () => {
wrapper.find('.peopletable-names-showall').hostNodes().simulate('click');
expect(store.getActions()).to.eql([{ type: 'SET_IS_PATIENT_LIST_VISIBLE', payload: { isVisible: true } }])
})
@@ -664,7 +670,6 @@ describe('ClinicPatients', () => {
'clinicID123',
{
fullName: 'Patient Name',
- connectDexcom: false,
birthDate: '1999-11-21',
mrn: '123456',
email: 'patient@test.ca',
@@ -1054,10 +1059,6 @@ describe('ClinicPatients', () => {
patientForm().find('input[name="email"]').simulate('change', { persist: noop, target: { name: 'email', value: 'patient-two@test.ca' } });
expect(patientForm().find('input[name="email"]').prop('value')).to.equal('patient-two@test.ca');
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().checked).to.be.false;
- patientForm().find('input[name="connectDexcom"]').find('input').simulate('change', { persist: noop, target: { name: 'connectDexcom', checked: true, value: true } });
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().checked).to.be.true;
-
store.clearActions();
dialog().find('Button#editPatientConfirm').simulate('click');
@@ -1070,8 +1071,6 @@ describe('ClinicPatients', () => {
'patient2',
{
fullName: 'Patient 2',
- connectDexcom: true,
- dataSources: [{ providerName: 'dexcom', state: 'pending' }],
birthDate: '1999-01-01',
mrn: 'MRN456',
id: 'patient2',
@@ -1145,7 +1144,6 @@ describe('ClinicPatients', () => {
'patient1',
{
fullName: 'Patient 2',
- connectDexcom: false,
birthDate: '1999-02-02',
mrn: 'MRN456',
id: 'patient1',
@@ -1172,6 +1170,23 @@ describe('ClinicPatients', () => {
}, 1000);
});
+ it('should open a modal for managing data connections when data connection menu option is clicked', () => {
+ const table = wrapper.find(Table);
+ expect(table).to.have.length(1);
+ expect(table.find('tr')).to.have.length(3); // header row + 2 invites
+ const dataConnectionsButton = table.find('tr').at(2).find('Button[iconLabel="Bring Data into Tidepool"]');
+ const dialog = () => wrapper.find('Dialog#data-connections');
+ expect(dialog()).to.have.length(0);
+
+ dataConnectionsButton.simulate('click');
+ wrapper.update();
+ expect(dialog()).to.have.length(1);
+ expect(dialog().props().open).to.be.true;
+
+ expect(defaultProps.trackMetric.calledWith('Clinic - Edit patient data connections')).to.be.true;
+ expect(defaultProps.trackMetric.callCount).to.equal(1);
+ });
+
it('should remove a patient', () => {
const table = wrapper.find(Table);
expect(table).to.have.length(1);
@@ -1202,206 +1217,6 @@ describe('ClinicPatients', () => {
expect(defaultProps.trackMetric.callCount).to.equal(2);
});
- context('dexcom connection status - patient add', () => {
- let patientForm;
-
- beforeEach(() => {
- const addButton = wrapper.find('button#add-patient');
- expect(addButton.text()).to.equal('Add New Patient');
-
- const dialog = () => wrapper.find('Dialog#addPatient');
-
- expect(dialog()).to.have.length(0);
- addButton.simulate('click');
- wrapper.update();
- expect(dialog()).to.have.length(1);
- expect(dialog().props().open).to.be.true;
-
- patientForm = () => dialog().find('form#clinic-patient-form');
- expect(patientForm()).to.have.lengthOf(1);
- });
-
- it('should render the dexcom connect request input', () => {
- expect(patientForm().find('input[name="connectDexcom"]').hostNodes()).to.have.lengthOf(1);
- });
-
- it('should disable the dexcom connect input if email is empty', () => {
- expect(patientForm().find('input[name="email"]').prop('value')).to.equal('');
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().disabled).to.be.true;
-
- patientForm().find('input[name="email"]').simulate('change', { persist: noop, target: { name: 'email', value: 'patient-two@test.ca' } });
- expect(patientForm().find('input[name="email"]').prop('value')).to.equal('patient-two@test.ca');
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().disabled).to.be.false;
- });
-
- it('should disable and uncheck the dexcom connect checkbox if email is cleared', () => {
- // Set the email and check the dexcom request box
- patientForm().find('input[name="email"]').simulate('change', { persist: noop, target: { name: 'email', value: 'patient-two@test.ca' } });
- expect(patientForm().find('input[name="email"]').prop('value')).to.equal('patient-two@test.ca');
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().disabled).to.be.false;
-
- patientForm().find('input[name="connectDexcom"]').find('input').simulate('change', { persist: noop, target: { name: 'connectDexcom', checked: true, value: true } });
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().checked).to.be.true;
-
- // Clear the email input
- patientForm().find('input[name="email"]').simulate('change', { persist: noop, target: { name: 'email', value: '' } });
- expect(patientForm().find('input[name="email"]').prop('value')).to.equal('');
-
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().disabled).to.be.true;
- expect(patientForm().find('input[name="connectDexcom"]').find('input').props().checked).to.be.false;
- });
- });
-
- context('dexcom connection status - patient edit', () => {
- let patientForm;
-
- const getPatientForm = (patientIndex) => {
- const table = wrapper.find(Table);
- const editButton = table.find('tbody tr').at(patientIndex).find('Button[iconLabel="Edit Patient Information"]');
- const dialog = () => wrapper.find('Dialog#editPatient');
-
- editButton.simulate('click');
- wrapper.update();
- expect(dialog()).to.have.length(1);
- expect(dialog().props().open).to.be.true;
-
- patientForm = () => dialog().find('form#clinic-patient-form');
- expect(patientForm()).to.have.lengthOf(1);
- }
-
- beforeEach(() => {
- store = mockStore(dexcomPatientsClinicState);
- defaultProps.trackMetric.resetHistory();
- wrapper = mount(
-
-
-
-
-
- );
-
- defaultProps.trackMetric.resetHistory();
-
- getPatientForm(0);
- });
-
- it('should render the dexcom connect request input, but only if the patient does not have a dexcom data source', () => {
- getPatientForm(5); // no dexcom source
- expect(patientForm().find('#connectDexcomWrapper').hostNodes()).to.have.lengthOf(1)
-
- getPatientForm(0); // pending dexcom state
- expect(patientForm().find('#connectDexcomWrapper').hostNodes()).to.have.lengthOf(0)
- });
-
- it('should show the current dexcom connection status if the patient has it set', () => {
- const stateWrapper = () => patientForm().find('#connectDexcomStatusWrapper').hostNodes();
-
- getPatientForm(0);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Pending connection with');
-
- getPatientForm(1);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Connected with');
-
- getPatientForm(2);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Disconnected from');
-
- getPatientForm(3);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Error connecting to');
-
- getPatientForm(4);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Unknown connection to');
-
- getPatientForm(6);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Pending reconnection with');
- });
-
- it('should have a valid form state for all legitimate dexcom connection states', () => {
- const stateWrapper = () => patientForm().find('#connectDexcomStatusWrapper').hostNodes();
- const submitButton = () => wrapper.find('#editPatientConfirm').hostNodes();
-
- getPatientForm(0);
- expect(stateWrapper().text()).includes('Pending connection with');
- expect(submitButton().prop('disabled')).to.be.false;
-
- getPatientForm(1);
- expect(stateWrapper().text()).includes('Connected with');
- expect(submitButton().prop('disabled')).to.be.false;
-
- getPatientForm(2);
- expect(stateWrapper().text()).includes('Disconnected from');
- expect(submitButton().prop('disabled')).to.be.false;
-
- getPatientForm(3);
- expect(stateWrapper().text()).includes('Error connecting to');
- expect(submitButton().prop('disabled')).to.be.false;
-
- getPatientForm(6);
- expect(stateWrapper().text()).includes('Pending reconnection with');
- expect(submitButton().prop('disabled')).to.be.false;
- });
-
- it('should allow resending a pending dexcom connection reminder', () => {
- const stateWrapper = () => patientForm().find('#connectDexcomStatusWrapper').hostNodes();
- const resendButton = () => stateWrapper().find('#resendDexcomConnectRequestTrigger').hostNodes();
-
- getPatientForm(1);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Connected with');
- expect(resendButton()).to.have.lengthOf(0);
-
- // Show for disconnected state
- getPatientForm(2);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Disconnected from');
- expect(resendButton()).to.have.lengthOf(1);
-
- // Show for pending state
- getPatientForm(0);
- expect(stateWrapper()).to.have.lengthOf(1);
- expect(stateWrapper().text()).includes('Pending connection');
- expect(resendButton()).to.have.lengthOf(1);
-
- const resendDialog = () => stateWrapper().find('#resendDexcomConnectRequest').at(1);
- expect(resendDialog().props().open).to.be.false;
- resendButton().simulate('click');
- expect(resendDialog().props().open).to.be.true;
-
- expect(resendDialog().text()).to.have.string('10/19/2021 at 4:27 pm');
-
- const resendInvite = resendDialog().find(Button).filter({variant: 'primary'});
- expect(resendInvite).to.have.length(1);
-
- const expectedActions = [
- {
- type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST',
- },
- {
- type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS',
- payload: {
- clinicId: 'clinicID123',
- lastRequestedDexcomConnectTime: '2022-02-02T00:00:00.000Z',
- patientId: 'patient1',
- },
- },
- ];
-
- store.clearActions();
- resendInvite.props().onClick();
- expect(store.getActions()).to.eql(expectedActions);
- sinon.assert.calledWith(
- defaultProps.api.clinics.sendPatientDexcomConnectRequest,
- 'clinicID123',
- 'patient1'
- );
- });
- });
-
context('tier0100 clinic', () => {
beforeEach(() => {
store = mockStore(tier0100ClinicState);
@@ -2902,7 +2717,6 @@ describe('ClinicPatients', () => {
email: 'patient2@test.ca',
fullName: 'Patient Two',
birthDate: '1999-02-02',
- connectDexcom: false,
mrn: 'MRN123',
permissions: { custodian : undefined },
summary: {
@@ -2949,7 +2763,7 @@ describe('ClinicPatients', () => {
done();
}, 0);
- })
+ });
});
describe('Accessing TIDE dashboard', () => {
diff --git a/test/unit/pages/ClinicianPatients.test.js b/test/unit/pages/ClinicianPatients.test.js
index 89932b08d7..9008e9809d 100644
--- a/test/unit/pages/ClinicianPatients.test.js
+++ b/test/unit/pages/ClinicianPatients.test.js
@@ -120,7 +120,7 @@ describe('ClinicianPatients', () => {
removingMembershipInOtherCareTeam: defaultWorkingState,
updatingPatient: defaultWorkingState,
creatingVCACustodialAccount: defaultWorkingState,
- sendingPatientDexcomConnectRequest: defaultWorkingState,
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
fetchingPatientsForClinic: defaultWorkingState,
},
patientListFilters: {
diff --git a/test/unit/pages/OAuthConnection.test.js b/test/unit/pages/OAuthConnection.test.js
index fa43b37428..68f02b9240 100644
--- a/test/unit/pages/OAuthConnection.test.js
+++ b/test/unit/pages/OAuthConnection.test.js
@@ -218,4 +218,159 @@ describe('OAuthConnection', () => {
expect(custodialWrapper.find('#oauth-claim-account-button').hostNodes()).to.have.lengthOf(0);
});
});
+
+ context('abbott authorized', () => {
+ beforeEach(() => {
+ wrapper = createWrapper('abbott', 'authorized');
+ });
+
+ it('should track the appropriate metric on load', () => {
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection', {
+ providerName: 'abbott',
+ status: 'authorized',
+ custodialSignup: false,
+ });
+
+ defaultProps.trackMetric.resetHistory();
+ createWrapper('abbott', 'authorized', '?signupKey=abc&signupEmail=patient@mail.com');
+
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection', {
+ providerName: 'abbott',
+ status: 'authorized',
+ custodialSignup: true,
+ });
+ });
+
+ it('should render the appropriate banner', () => {
+ expect(wrapper.find('#banner-oauth-authorized').hostNodes().text()).to.equal('You have successfully connected your Abbott account to Tidepool.');
+ });
+
+ it('should render the appropriate heading and subheading', () => {
+ expect(wrapper.find('#oauth-heading').hostNodes().text()).to.equal('Connection Authorized');
+ expect(wrapper.find('#oauth-subheading').hostNodes().text()).to.equal('Thank you for connecting with Tidepool!');
+ });
+
+ it('should render the appropriate message text', () => {
+ expect(wrapper.find('#oauth-message').hostNodes().text()).to.equal('We hope you enjoy your Tidepool experience.');
+ });
+
+ it('should render a button that claims an account if the signup query params are provided', () => {
+ const custodialWrapper = createWrapper('abbott', 'authorized', '?signupKey=abc&signupEmail=patient@mail.com');
+ expect(wrapper.find('#oauth-claim-account-button').hostNodes()).to.have.lengthOf(0);
+ expect(custodialWrapper.find('#oauth-claim-account-button').hostNodes()).to.have.lengthOf(1);
+
+ defaultProps.trackMetric.resetHistory();
+ custodialWrapper.find('#oauth-claim-account-button').hostNodes().simulate('click');
+
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection - Claim Account', {
+ providerName: 'abbott',
+ status: 'authorized',
+ });
+
+ let expectedActions = [
+ routeAction('/login?signupKey=abc&signupEmail=patient%40mail.com'),
+ ];
+
+ const actions = store.getActions();
+ expect(actions).to.eql(expectedActions);
+ });
+ });
+
+ context('abbott declined', () => {
+ beforeEach(() => {
+ wrapper = createWrapper('abbott', 'declined');
+ });
+
+ it('should track the appropriate metric on load', () => {
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection', {
+ providerName: 'abbott',
+ status: 'declined',
+ custodialSignup: false,
+ });
+
+ defaultProps.trackMetric.resetHistory();
+ createWrapper('abbott', 'declined', '?signupKey=abc&signupEmail=patient@mail.com');
+
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection', {
+ providerName: 'abbott',
+ status: 'declined',
+ custodialSignup: true,
+ });
+ });
+
+ it('should render the appropriate banner', () => {
+ expect(wrapper.find('#banner-oauth-declined').hostNodes().text()).to.equal('You have declined connecting your Abbott account to Tidepool.');
+ });
+
+ it('should render the appropriate heading and subheading', () => {
+ expect(wrapper.find('#oauth-heading').hostNodes().text()).to.equal('Connection Declined');
+ expect(wrapper.find('#oauth-subheading').hostNodes().text()).to.equal('You can always decide to connect at a later time.');
+ });
+
+ it('should render the appropriate message text', () => {
+ expect(wrapper.find('#oauth-message').hostNodes().text()).to.equal('We hope you enjoy your Tidepool experience.');
+ });
+
+ it('should render a button that claims an account if the signup query params are provided', () => {
+ const custodialWrapper = createWrapper('abbott', 'declined', '?signupKey=abc&signupEmail=patient@mail.com');
+ expect(wrapper.find('#oauth-claim-account-button').hostNodes()).to.have.lengthOf(0);
+ expect(custodialWrapper.find('#oauth-claim-account-button').hostNodes()).to.have.lengthOf(1);
+
+ defaultProps.trackMetric.resetHistory();
+ custodialWrapper.find('#oauth-claim-account-button').hostNodes().simulate('click');
+
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection - Claim Account', {
+ providerName: 'abbott',
+ status: 'declined',
+ });
+
+ let expectedActions = [
+ routeAction('/login?signupKey=abc&signupEmail=patient%40mail.com'),
+ ];
+
+ const actions = store.getActions();
+ expect(actions).to.eql(expectedActions);
+ });
+ });
+
+ context('abbott error', () => {
+ beforeEach(() => {
+ wrapper = createWrapper('abbott', 'error');
+ });
+
+ it('should track the appropriate metric on load', () => {
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection', {
+ providerName: 'abbott',
+ status: 'error',
+ custodialSignup: false,
+ });
+
+ defaultProps.trackMetric.resetHistory();
+ createWrapper('abbott', 'error', '?signupKey=abc&signupEmail=patient@mail.com');
+
+ sinon.assert.calledWith(defaultProps.trackMetric, 'Oauth - Connection', {
+ providerName: 'abbott',
+ status: 'error',
+ custodialSignup: true,
+ });
+ });
+
+ it('should render the appropriate banner', () => {
+ expect(wrapper.find('#banner-oauth-error').hostNodes().text()).to.equal('We were unable to determine your Abbott connection status.');
+ });
+
+ it('should render the appropriate heading and subheading', () => {
+ expect(wrapper.find('#oauth-heading').hostNodes().text()).to.equal('Connection Error');
+ expect(wrapper.find('#oauth-subheading').hostNodes().text()).to.equal('Hmm... That didn\'t work. Please try again.');
+ });
+
+ it('should not render any secondary message text', () => {
+ expect(wrapper.find('#oauth-message').hostNodes()).to.have.lengthOf(0);
+ });
+
+ it('should NOT render a button that claims an account if the signup query params are provided', () => {
+ const custodialWrapper = createWrapper('abbott', 'error', '?signupKey=abc&signupEmail=patient@mail.com');
+ expect(custodialWrapper.find('#oauth-claim-account-button').hostNodes()).to.have.lengthOf(0);
+ });
+ });
});
diff --git a/test/unit/pages/TideDashboard.test.js b/test/unit/pages/TideDashboard.test.js
index 1f1e4deb03..6bf0e9a3e5 100644
--- a/test/unit/pages/TideDashboard.test.js
+++ b/test/unit/pages/TideDashboard.test.js
@@ -13,6 +13,8 @@ import { ToastProvider } from '../../../app/providers/ToastProvider';
import TideDashboard from '../../../app/pages/dashboard/TideDashboard';
import Popover from '../../../app/components/elements/Popover';
import TideDashboardConfigForm from '../../../app/components/clinic/TideDashboardConfigForm';
+import DataConnections from '../../../app/components/datasources/DataConnections';
+import DataConnectionsModal from '../../../app/components/datasources/DataConnectionsModal';
import { clinicUIDetails } from '../../../app/core/clinicUtils';
import mockTideDashboardPatients from '../../fixtures/mockTideDashboardPatients.json';
import LDClientMock from '../../fixtures/LDClientMock';
@@ -63,6 +65,9 @@ describe('TideDashboard', () => {
showSummaryDashboard: true,
}));
+ DataConnections.__Rewire__('api', defaultProps.api);
+ DataConnectionsModal.__Rewire__('api', defaultProps.api);
+
TideDashboard.__Rewire__('useLocation', sinon.stub().returns({
search: '',
pathname: '/dashboard/tide'
@@ -76,6 +81,8 @@ describe('TideDashboard', () => {
afterEach(() => {
TideDashboard.__ResetDependency__('useLDClient');
TideDashboard.__ResetDependency__('useFlags');
+ DataConnections.__ResetDependency__('api');
+ DataConnectionsModal.__ResetDependency__('api');
});
const sampleTags = [
@@ -199,7 +206,7 @@ describe('TideDashboard', () => {
fetchingPatientFromClinic: defaultWorkingState,
fetchingTideDashboardPatients: completedState,
updatingClinicPatient: defaultWorkingState,
- sendingPatientDexcomConnectRequest: defaultWorkingState,
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
settingClinicPatientLastReviewed: defaultWorkingState,
revertingClinicPatientLastReviewed: defaultWorkingState,
},
@@ -599,7 +606,7 @@ describe('TideDashboard', () => {
expect(getTableRow(0, 0).find('th').at(9).text()).contains('Tags');
expect(getTableRow(0, 2).find('td').at(8).text()).contains('test tag 1');
- // Should contain a "more" menu that allows opening a patient edit dialog
+ // Should contain a "more" menu that allows opening a patient edit dialog and opening a patient data connections dialog
const moreMenuIcon = getTableRow(0, 2).find('td').at(9).find('PopoverMenu').find('Icon').at(0);
const popoverMenu = () => wrapper.find(Popover).at(4);
expect(popoverMenu().props().open).to.be.false;
@@ -609,16 +616,30 @@ describe('TideDashboard', () => {
const editButton = popoverMenu().find('Button[iconLabel="Edit Patient Information"]');
expect(editButton).to.have.lengthOf(1);
- const dialog = () => wrapper.find('Dialog#editPatient');
- expect(dialog()).to.have.length(0);
+ const editDialog = () => wrapper.find('Dialog#editPatient');
+ expect(editDialog()).to.have.length(0);
editButton.simulate('click');
wrapper.update();
- expect(dialog()).to.have.length(1);
- expect(dialog().props().open).to.be.true;
+ expect(editDialog()).to.have.length(1);
+ expect(editDialog().props().open).to.be.true;
expect(defaultProps.trackMetric.calledWith('Clinic - Edit patient')).to.be.true;
expect(defaultProps.trackMetric.callCount).to.equal(1);
+ const dataConnectionsButton = popoverMenu().find('Button[iconLabel="Bring Data into Tidepool"]');
+ expect(dataConnectionsButton).to.have.lengthOf(1);
+
+ const dataConnectionsDialog = () => wrapper.find('Dialog#data-connections');
+ expect(dataConnectionsDialog()).to.have.length(0);
+
+ dataConnectionsButton.simulate('click');
+ wrapper.update();
+ expect(dataConnectionsDialog()).to.have.length(1);
+ expect(dataConnectionsDialog().props().open).to.be.true;
+
+ expect(defaultProps.trackMetric.calledWith('Clinic - Edit patient data connections')).to.be.true;
+ expect(defaultProps.trackMetric.callCount).to.equal(2);
+
// Confirm second table is sorted appropriately
expect(getTableRow(1, 0).find('th').at(5).text()).contains('% Time 54-70');
expect(getTableRow(1, 1).find('td').at(4).text()).contains('9 %');
diff --git a/test/unit/pages/dashboard/PatientDrawer/CGMStatistics.test.js b/test/unit/pages/dashboard/PatientDrawer/CGMStatistics.test.js
deleted file mode 100644
index b395364cbd..0000000000
--- a/test/unit/pages/dashboard/PatientDrawer/CGMStatistics.test.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/* global chai */
-/* global describe */
-/* global sinon */
-/* global afterEach */
-/* global context */
-/* global it */
-/* global beforeEach */
-
-import React from 'react';
-import { mount } from 'enzyme';
-import _ from 'lodash';
-
-import CGMStatistics from '../../../../../app/pages/dashboard/PatientDrawer/CGMStatistics';
-
-const expect = chai.expect;
-
-const agpCGM = {
- 'data': {
- 'current': {
- 'aggregationsByDate': {},
- 'stats': {
- 'averageGlucose': {
- 'averageGlucose': 121.24147339699863,
- 'total': 733
- },
- 'bgExtents': {
- 'bgMax': 350,
- 'bgMin': 39,
- 'bgDaysWorn': 7,
- 'newestDatum': { 'time': 1733164212000 },
- 'oldestDatum': { 'time': 1732608015000 }
- },
- 'coefficientOfVariation': {
- 'coefficientOfVariation': 49.838071869255685,
- 'total': 733
- },
- 'glucoseManagementIndicator': {
- 'glucoseManagementIndicator': null,
- 'glucoseManagementIndicatorAGP': 6.210096043656208,
- 'insufficientData': true
- },
- 'sensorUsage': {
- 'sensorUsage': 219900000,
- 'sensorUsageAGP': 39.514824797843666,
- 'total': 604800000,
- 'sampleFrequency': 300000,
- 'count': 733
- },
- 'timeInRange': {
- 'durations': {
- 'veryLow': 5186357.435197817,
- 'low': 12376534.788540244,
- 'target': 56106957.70804911,
- 'high': 8368894.952251023,
- 'veryHigh': 4361255.115961801,
- 'total': 219900000
- },
- 'counts': {
- 'veryLow': 44,
- 'low': 105,
- 'target': 476,
- 'high': 71,
- 'veryHigh': 37,
- 'total': 733
- }
- }
- },
- 'endpoints': {
- 'range': [
- 1732608000000,
- 1733212800000
- ],
- 'days': 7,
- 'activeDays': 7
- }
- }
- },
- 'timePrefs': {
- 'timezoneAware': true,
- 'timezoneName': 'US/Eastern'
- },
- 'bgPrefs': {
- 'bgUnits': 'mg/dL'
- },
- 'query': {},
- 'metaData': {},
-};
-
-describe('PatientDrawer/CGMStatistics', () => {
- describe('When data is not present', () => {
- const wrapper = mount();
-
- it('renders no data', () => {
- expect(wrapper.isEmptyRender()).to.be.true;
- });
- });
-
- describe('When data is in mg/dL', () => {
- const wrapper = mount();
-
- it('renders the time range in the expected format', () => {
- expect(wrapper.find('#agp-table-time-range').hostNodes().text()).to.include('November 26 - December 2, 2024 (7 days)');
- });
-
- it('renders the CGM Active % in the expected format', () => {
- expect(wrapper.find('#agp-table-cgm-active').hostNodes().text()).to.include('39.5%');
- });
-
- it('renders the Average Glucose in the expected format', () => {
- expect(wrapper.find('#agp-table-avg-glucose').hostNodes().text()).to.include('121 mg/dL');
- });
-
- it('renders the GMI in the expected format', () => {
- expect(wrapper.find('#agp-table-gmi').hostNodes().text()).to.include('6.2%');
- });
-
- it('renders the Glucose Variability in the expected format', () => {
- expect(wrapper.find('#agp-table-cov').hostNodes().text()).to.include('49.8%');
- });
- });
-});
diff --git a/test/unit/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js b/test/unit/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js
new file mode 100644
index 0000000000..226e47b9e5
--- /dev/null
+++ b/test/unit/pages/dashboard/PatientDrawer/CGMStatistics/index.test.js
@@ -0,0 +1,168 @@
+/* global chai */
+/* global describe */
+/* global sinon */
+/* global afterEach */
+/* global context */
+/* global it */
+/* global beforeEach */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import CGMStatistics from '../../../../../../app/pages/dashboard/PatientDrawer/CGMStatistics';
+
+const expect = chai.expect;
+
+const agpCGM = {
+ 'data': {
+ 'current': {
+ 'aggregationsByDate': {},
+ 'stats': {
+ 'averageGlucose': {
+ 'averageGlucose': 121.4013071783735,
+ 'total': 8442,
+ },
+ 'bgExtents': {
+ 'bgMax': 401.00001001500004,
+ 'bgMin': 38.9999690761,
+ 'bgDaysWorn': 30,
+ 'newestDatum': { 'time': 1736783190269 },
+ 'oldestDatum': { 'time': 1734249705225 },
+ },
+ 'coefficientOfVariation': {
+ 'coefficientOfVariation': 49.82047988955789,
+ 'total': 8442,
+ },
+ 'glucoseManagementIndicator': {
+ 'glucoseManagementIndicator': 6.236871267706695,
+ 'glucoseManagementIndicatorAGP': 6.236871267706695,
+ 'total': 8442,
+ },
+ 'sensorUsage': {
+ 'sensorUsage': 2532600000,
+ 'sensorUsageAGP': 99.95264030310206,
+ 'total': 2592000000,
+ 'sampleFrequency': 300000,
+ 'count': 8442,
+ },
+ 'timeInRange': {
+ 'durations': {
+ 'veryLow': 337739.8720682303,
+ 'low': 2507462.686567164,
+ 'target': 66023027.7185501,
+ 'high': 14031556.503198294,
+ 'veryHigh': 3500213.219616205,
+ 'total': 2532600000,
+ },
+ 'counts': {
+ 'veryLow': 33,
+ 'low': 245,
+ 'target': 6451,
+ 'high': 1371,
+ 'veryHigh': 342,
+ 'total': 8442,
+ },
+ },
+ },
+ 'endpoints': {
+ 'range': [1734249600000, 1736841600000 ],
+ 'days': 30,
+ 'activeDays': 30,
+ },
+ 'data': {},
+ },
+ },
+ 'timePrefs': { 'timezoneAware': true, 'timezoneName': 'Etc/GMT+8' },
+ 'bgPrefs': {
+ 'bgBounds': {
+ 'veryHighThreshold': 250,
+ 'targetUpperBound': 180,
+ 'targetLowerBound': 70,
+ 'veryLowThreshold': 54,
+ 'extremeHighThreshold': 350,
+ 'clampThreshold': 600,
+ },
+ 'bgClasses': {
+ 'low': { 'boundary': 70 },
+ 'target': { 'boundary': 180 },
+ 'very-low': { 'boundary': 54 },
+ 'high': { 'boundary': 250 },
+ },
+ 'bgUnits': 'mg/dL',
+ },
+ 'query': {
+ 'endpoints': [
+ 1734249600000,
+ 1736841600000,
+ ],
+ 'aggregationsByDate': 'dataByDate, statsByDate',
+ 'bgSource': 'cbg',
+ 'stats': [
+ 'averageGlucose',
+ 'bgExtents',
+ 'coefficientOfVariation',
+ 'glucoseManagementIndicator',
+ 'sensorUsage',
+ 'timeInRange',
+ ],
+ 'types': { 'cbg': {} },
+ 'bgPrefs': {
+ 'bgUnits': 'mg/dL',
+ 'bgClasses': {
+ 'low': { 'boundary': 70 },
+ 'target': { 'boundary': 180 },
+ 'very-low': { 'boundary': 54 },
+ 'high': { 'boundary': 250 },
+ },
+ 'bgBounds': {
+ 'veryHighThreshold': 250,
+ 'targetUpperBound': 180,
+ 'targetLowerBound': 70,
+ 'veryLowThreshold': 54,
+ 'extremeHighThreshold': 350,
+ 'clampThreshold': 600,
+ },
+ },
+ 'metaData': 'latestPumpUpload, bgSources',
+ 'timePrefs': {
+ 'timezoneAware': true,
+ 'timezoneName': 'Etc/GMT+8',
+ },
+ 'excludedDevices': [],
+ },
+ 'metaData': {},
+};
+
+describe('PatientDrawer/CGMStatistics', () => {
+ describe('When data is not present', () => {
+ const wrapper = mount();
+
+ it('renders no data', () => {
+ expect(wrapper.isEmptyRender()).to.be.true;
+ });
+ });
+
+ describe('When data is in mg/dL', () => {
+ const wrapper = mount();
+
+ it('renders the time range in the expected format', () => {
+ expect(wrapper.find('#agp-table-time-range').hostNodes().text()).to.include('December 15, 2024 - January 13, 2025 (30 days)');
+ });
+
+ it('renders the CGM Active % in the expected format', () => {
+ expect(wrapper.find('#agp-table-cgm-active').hostNodes().text()).to.include('100%');
+ });
+
+ it('renders the Average Glucose in the expected format', () => {
+ expect(wrapper.find('#agp-table-avg-glucose').hostNodes().text()).to.include('121 mg/dL');
+ });
+
+ it('renders the GMI in the expected format', () => {
+ expect(wrapper.find('#agp-table-gmi').hostNodes().text()).to.include('6.2%');
+ });
+
+ it('renders the Glucose Variability in the expected format', () => {
+ expect(wrapper.find('#agp-table-cov').hostNodes().text()).to.include('49.8%');
+ });
+ });
+});
diff --git a/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js b/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js
new file mode 100644
index 0000000000..f9dca3090a
--- /dev/null
+++ b/test/unit/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton.test.js
@@ -0,0 +1,98 @@
+/* global chai */
+/* global describe */
+/* global sinon */
+/* global afterEach */
+/* global context */
+/* global it */
+/* global before */
+/* global after */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import _ from 'lodash';
+
+import CGMClipboardButton from '../../../../../../app/pages/dashboard/PatientDrawer/MenuBar/CGMClipboardButton';
+
+const expect = chai.expect;
+
+const patient = {
+ birthDate: '2001-01-01',
+ email: 'tcrawford@test.test',
+ fullName: 'Terence Crawford',
+ id: '1234-abcd',
+};
+
+const pdf = {
+ 'data': { // truncated for brevity
+ 'agpCGM': {
+ 'data': {
+ 'current': {
+ 'stats': {
+ 'sensorUsage': {
+ 'sensorUsage': 2532600000,
+ 'sensorUsageAGP': 99.95264030310206,
+ 'total': 2592000000,
+ 'sampleFrequency': 300000,
+ 'count': 8442,
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+describe('PatientDrawer/MenuBar/CGMClipboardButton', () => {
+ CGMClipboardButton.__Rewire__('agpCGMText', sinon.stub().returns('AGP_DATA_STRING_TO_COPY'));
+
+ const writeTextSpy = sinon.stub(window?.navigator?.clipboard, 'writeText');
+
+ describe('When data is not present', () => {
+ const insufficientAgpCGM = null;
+ const wrapper = mount();
+
+ it('is disabled', () => {
+ expect(wrapper.find('button').props().disabled).to.be.true;
+ });
+ });
+
+ describe('When data is insufficient due to too few enough readings', () => {
+ const insufficientAgpCGM = _.cloneDeep(pdf.data.agpCGM);
+ insufficientAgpCGM.data.current.stats.sensorUsage.count = 100;
+
+ const wrapper = mount();
+
+ it('is disabled', () => {
+ expect(wrapper.find('button').props().disabled).to.be.true;
+ });
+ });
+
+ describe('When data is insufficient due to being BGM patient', () => {
+ const insufficientAgpCGM = _.cloneDeep(pdf.data.agpCGM);
+ insufficientAgpCGM.data.current.stats = {
+ sensorUsage: {
+ sensorUsage: 0,
+ sensorUsageAGP: 0,
+ total: 2592000000,
+ sampleFrequency: 300000,
+ count: 0,
+ },
+ };
+
+ const wrapper = mount();
+
+ it('is disabled', () => {
+ expect(wrapper.find('button').props().disabled).to.be.true;
+ });
+ });
+
+ describe('When data is present', () => {
+ const wrapper = mount();
+ wrapper.find('button').simulate('click');
+
+ it('calls writeText on navigator API with correct data', () => {
+ expect(wrapper.find('button').props().disabled).to.be.false;
+ expect(writeTextSpy.getCall(0).args[0]).to.eql('AGP_DATA_STRING_TO_COPY');
+ });
+ });
+});
diff --git a/test/unit/pages/share/PatientInvites.test.js b/test/unit/pages/share/PatientInvites.test.js
index 8b07fbd7c9..bf99b9b898 100644
--- a/test/unit/pages/share/PatientInvites.test.js
+++ b/test/unit/pages/share/PatientInvites.test.js
@@ -101,7 +101,7 @@ describe('PatientInvites', () => {
acceptingPatientInvitation: defaultWorkingState,
deletingPatientInvitation: defaultWorkingState,
deletingPatientInvitation: defaultWorkingState,
- sendingPatientDexcomConnectRequest: defaultWorkingState,
+ sendingPatientDataProviderConnectRequest: defaultWorkingState,
fetchingPatientsForClinic: defaultWorkingState,
},
},
diff --git a/test/unit/redux/actions/async.test.js b/test/unit/redux/actions/async.test.js
index 2fe8b72822..e833460060 100644
--- a/test/unit/redux/actions/async.test.js
+++ b/test/unit/redux/actions/async.test.js
@@ -8506,61 +8506,74 @@ describe('Actions', () => {
});
});
- describe('sendPatientDexcomConnectRequest', () => {
- it('should trigger SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS and it should call clinics.sendPatientDexcomConnectRequest once for a successful request', () => {
+ describe('sendPatientDataProviderConnectRequest', () => {
+ beforeEach(() => {
+ async.__Rewire__('moment', {
+ utc: () => ({ toISOString: () => '2022-02-02T00:00:00.000Z'})
+ });
+ });
+
+ afterEach(() => {
+ async.__ResetDependency__('moment');
+ });
+
+ it('should trigger SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS and it should call clinics.sendPatientDataProviderConnectRequest once for a successful request', () => {
const clinicId = 'clinicId1';
const patientId = 'patientId1';
- const lastRequestedDexcomConnectTime = '2022-10-10T00:00:000Z';
+ const providerName = 'dexcom';
+ const createdTime = '2022-02-02T00:00:00.000Z';
let api = {
clinics: {
- sendPatientDexcomConnectRequest: sinon.stub().callsArgWith(2, null, { id: patientId, lastRequestedDexcomConnectTime }),
+ sendPatientDataProviderConnectRequest: sinon.stub().callsArgWith(3, null),
},
};
let expectedActions = [
- { type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST' },
- { type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS', payload: { clinicId, patientId, lastRequestedDexcomConnectTime } }
+ { type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST' },
+ { type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS', payload: { clinicId, patientId, providerName, createdTime } }
];
_.each(expectedActions, (action) => {
expect(isTSA(action)).to.be.true;
});
let store = mockStore({ blip: initialState });
- store.dispatch(async.sendPatientDexcomConnectRequest(api, clinicId, patientId));
+ store.dispatch(async.sendPatientDataProviderConnectRequest(api, clinicId, patientId, providerName));
const actions = store.getActions();
expect(actions).to.eql(expectedActions);
- expect(api.clinics.sendPatientDexcomConnectRequest.callCount).to.equal(1);
+ expect(api.clinics.sendPatientDataProviderConnectRequest.callCount).to.equal(1);
});
- it('should trigger SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE and it should call error once for a failed request', () => {
+ it('should trigger SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE and it should call error once for a failed request', () => {
let clinicId = 'clinicId1';
const patientId = 'patientId1';
+ const providerName = 'dexcom';
+
let api = {
clinics: {
- sendPatientDexcomConnectRequest: sinon.stub().callsArgWith(2, {status: 500, body: 'Error!'}, null),
+ sendPatientDataProviderConnectRequest: sinon.stub().callsArgWith(3, {status: 500, body: 'Error!'}),
},
};
- let err = new Error(ErrorMessages.ERR_SENDING_PATIENT_DEXCOM_CONNECT_REQUEST);
+ let err = new Error(ErrorMessages.ERR_SENDING_PATIENT_DATA_PROVIDER_CONNECT_REQUEST);
err.status = 500;
let expectedActions = [
- { type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST' },
- { type: 'SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE', error: err, meta: { apiError: {status: 500, body: 'Error!'} } }
+ { type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST' },
+ { type: 'SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE', error: err, meta: { apiError: {status: 500, body: 'Error!'} } }
];
_.each(expectedActions, (action) => {
expect(isTSA(action)).to.be.true;
});
let store = mockStore({ blip: initialState });
- store.dispatch(async.sendPatientDexcomConnectRequest(api, clinicId, patientId));
+ store.dispatch(async.sendPatientDataProviderConnectRequest(api, clinicId, patientId, providerName));
const actions = store.getActions();
- expect(actions[1].error).to.deep.include({ message: ErrorMessages.ERR_SENDING_PATIENT_DEXCOM_CONNECT_REQUEST });
+ expect(actions[1].error).to.deep.include({ message: ErrorMessages.ERR_SENDING_PATIENT_DATA_PROVIDER_CONNECT_REQUEST });
expectedActions[1].error = actions[1].error;
expect(actions).to.eql(expectedActions);
- expect(api.clinics.sendPatientDexcomConnectRequest.callCount).to.equal(1);
+ expect(api.clinics.sendPatientDataProviderConnectRequest.callCount).to.equal(1);
});
});
diff --git a/test/unit/redux/actions/sync.test.js b/test/unit/redux/actions/sync.test.js
index e284d84e77..8ab3f62099 100644
--- a/test/unit/redux/actions/sync.test.js
+++ b/test/unit/redux/actions/sync.test.js
@@ -3900,48 +3900,50 @@ describe('Actions', () => {
});
});
- describe('sendPatientDexcomConnectRequestRequest', () => {
+ describe('sendPatientDataProviderConnectRequestRequest', () => {
it('should be a TSA', () => {
- let action = sync.sendPatientDexcomConnectRequestRequest();
+ let action = sync.sendPatientDataProviderConnectRequestRequest();
expect(isTSA(action)).to.be.true;
});
- it('type should equal SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST', () => {
- let action = sync.sendPatientDexcomConnectRequestRequest();
- expect(action.type).to.equal('SEND_PATIENT_DEXCOM_CONNECT_REQUEST_REQUEST');
+ it('type should equal SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST', () => {
+ let action = sync.sendPatientDataProviderConnectRequestRequest();
+ expect(action.type).to.equal('SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_REQUEST');
});
});
- describe('sendPatientDexcomConnectRequestSuccess', () => {
+ describe('sendPatientDataProviderConnectRequestSuccess', () => {
const clinicId = 'clinicId';
const patientId = 'patientId';
- const lastRequestedDexcomConnectTime = '2022-10-10T00:00:000Z';
+ const providerName = 'providerName';
+ const createdTime = '2022-10-10T00:00:000Z';
it('should be a TSA', () => {
- let action = sync.sendPatientDexcomConnectRequestSuccess(clinicId);
+ let action = sync.sendPatientDataProviderConnectRequestSuccess(clinicId);
expect(isTSA(action)).to.be.true;
});
- it('type should equal SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS', () => {
- let action = sync.sendPatientDexcomConnectRequestSuccess(clinicId, patientId, lastRequestedDexcomConnectTime);
- expect(action.type).to.equal('SEND_PATIENT_DEXCOM_CONNECT_REQUEST_SUCCESS');
+ it('type should equal SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS', () => {
+ let action = sync.sendPatientDataProviderConnectRequestSuccess(clinicId, patientId, providerName, createdTime);
+ expect(action.type).to.equal('SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_SUCCESS');
expect(action.payload.clinicId).to.equal('clinicId');
expect(action.payload.patientId).to.equal('patientId');
- expect(action.payload.lastRequestedDexcomConnectTime).to.equal('2022-10-10T00:00:000Z');
+ expect(action.payload.providerName).to.equal('providerName');
+ expect(action.payload.createdTime).to.equal('2022-10-10T00:00:000Z');
});
});
- describe('sendPatientDexcomConnectRequestFailure', () => {
+ describe('sendPatientDataProviderConnectRequestFailure', () => {
it('should be a TSA', () => {
let error = new Error('clinic migration failed :(');
- let action = sync.sendPatientDexcomConnectRequestFailure(error);
+ let action = sync.sendPatientDataProviderConnectRequestFailure(error);
expect(isTSA(action)).to.be.true;
});
- it('type should equal SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE and error should equal passed error', () => {
+ it('type should equal SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE and error should equal passed error', () => {
let error = new Error('stink :(');
- let action = sync.sendPatientDexcomConnectRequestFailure(error);
- expect(action.type).to.equal('SEND_PATIENT_DEXCOM_CONNECT_REQUEST_FAILURE');
+ let action = sync.sendPatientDataProviderConnectRequestFailure(error);
+ expect(action.type).to.equal('SEND_PATIENT_DATA_PROVIDER_CONNECT_REQUEST_FAILURE');
expect(action.error).to.equal(error);
});
});
diff --git a/test/unit/redux/reducers/clinics.test.js b/test/unit/redux/reducers/clinics.test.js
index 7d489111ef..7146eb6526 100644
--- a/test/unit/redux/reducers/clinics.test.js
+++ b/test/unit/redux/reducers/clinics.test.js
@@ -774,11 +774,12 @@ describe('clinics', () => {
});
});
- describe('sendPatientDexcomConnectRequestSuccess', () => {
- it('should update patient `lastRequestedDexcomConnectTime` in state', () => {
+ describe('sendPatientDataProviderConnectRequestSuccess', () => {
+ it('should update patient `createdTime` in state', () => {
let clinicId = 'clinicId123';
let patientId = 'patientId123';
- const lastRequestedDexcomConnectTime = '2022-10-10T00:00:000Z';
+ let providerName = 'providerName';
+ const createdTime = '2022-10-10T00:00:000Z';
let initialStateForTest = {
[clinicId]: {
id: clinicId,
@@ -787,9 +788,9 @@ describe('clinics', () => {
},
},
};
- let action = actions.sync.sendPatientDexcomConnectRequestSuccess(clinicId, patientId, lastRequestedDexcomConnectTime);
+ let action = actions.sync.sendPatientDataProviderConnectRequestSuccess(clinicId, patientId, providerName, createdTime);
let state = reducer(initialStateForTest, action);
- expect(state.clinicId123.patients.patientId123.lastRequestedDexcomConnectTime).to.eql(lastRequestedDexcomConnectTime);
+ expect(state.clinicId123.patients.patientId123.connectionRequests[providerName]).to.eql([{ providerName, createdTime }]);
});
});
diff --git a/test/unit/redux/reducers/working.test.js b/test/unit/redux/reducers/working.test.js
index 5e0e3709c8..177f50c92e 100644
--- a/test/unit/redux/reducers/working.test.js
+++ b/test/unit/redux/reducers/working.test.js
@@ -7700,99 +7700,99 @@ describe('dataWorkerQueryData', () => {
});
});
- describe('sendPatientDexcomConnectRequest', () => {
+ describe('sendPatientDataProviderConnectRequest', () => {
describe('request', () => {
- it('should set sendingPatientDexcomConnectRequest.completed to null', () => {
- expect(initialState.sendingPatientDexcomConnectRequest.completed).to.be.null;
+ it('should set sendingPatientDataProviderConnectRequest.completed to null', () => {
+ expect(initialState.sendingPatientDataProviderConnectRequest.completed).to.be.null;
- let requestAction = actions.sync.sendPatientDexcomConnectRequestRequest();
+ let requestAction = actions.sync.sendPatientDataProviderConnectRequestRequest();
let requestState = reducer(initialState, requestAction);
- expect(requestState.sendingPatientDexcomConnectRequest.completed).to.be.null;
+ expect(requestState.sendingPatientDataProviderConnectRequest.completed).to.be.null;
- let successAction = actions.sync.sendPatientDexcomConnectRequestSuccess('foo');
+ let successAction = actions.sync.sendPatientDataProviderConnectRequestSuccess('foo');
let successState = reducer(requestState, successAction);
- expect(successState.sendingPatientDexcomConnectRequest.completed).to.be.true;
+ expect(successState.sendingPatientDataProviderConnectRequest.completed).to.be.true;
let state = reducer(successState, requestAction);
- expect(state.sendingPatientDexcomConnectRequest.completed).to.be.null;
+ expect(state.sendingPatientDataProviderConnectRequest.completed).to.be.null;
expect(mutationTracker.hasMutated(tracked)).to.be.false;
});
- it('should set sendingPatientDexcomConnectRequest.inProgress to be true', () => {
+ it('should set sendingPatientDataProviderConnectRequest.inProgress to be true', () => {
let initialStateForTest = _.merge({}, initialState);
let tracked = mutationTracker.trackObj(initialStateForTest);
- let action = actions.sync.sendPatientDexcomConnectRequestRequest();
+ let action = actions.sync.sendPatientDataProviderConnectRequestRequest();
- expect(initialStateForTest.sendingPatientDexcomConnectRequest.inProgress).to.be.false;
+ expect(initialStateForTest.sendingPatientDataProviderConnectRequest.inProgress).to.be.false;
let state = reducer(initialStateForTest, action);
- expect(state.sendingPatientDexcomConnectRequest.inProgress).to.be.true;
+ expect(state.sendingPatientDataProviderConnectRequest.inProgress).to.be.true;
expect(mutationTracker.hasMutated(tracked)).to.be.false;
});
});
describe('failure', () => {
- it('should set sendingPatientDexcomConnectRequest.completed to be false', () => {
+ it('should set sendingPatientDataProviderConnectRequest.completed to be false', () => {
let error = new Error('Something bad happened :(');
- expect(initialState.sendingPatientDexcomConnectRequest.completed).to.be.null;
+ expect(initialState.sendingPatientDataProviderConnectRequest.completed).to.be.null;
- let failureAction = actions.sync.sendPatientDexcomConnectRequestFailure(error);
+ let failureAction = actions.sync.sendPatientDataProviderConnectRequestFailure(error);
let state = reducer(initialState, failureAction);
- expect(state.sendingPatientDexcomConnectRequest.completed).to.be.false;
+ expect(state.sendingPatientDataProviderConnectRequest.completed).to.be.false;
expect(mutationTracker.hasMutated(tracked)).to.be.false;
});
- it('should set sendingPatientDexcomConnectRequest.inProgress to be false and set error', () => {
+ it('should set sendingPatientDataProviderConnectRequest.inProgress to be false and set error', () => {
let initialStateForTest = _.merge({}, initialState, {
- sendingPatientDexcomConnectRequest: { inProgress: true, notification: null },
+ sendingPatientDataProviderConnectRequest: { inProgress: true, notification: null },
});
let tracked = mutationTracker.trackObj(initialStateForTest);
let error = new Error('Something bad happened :(');
- let action = actions.sync.sendPatientDexcomConnectRequestFailure(error);
+ let action = actions.sync.sendPatientDataProviderConnectRequestFailure(error);
- expect(initialStateForTest.sendingPatientDexcomConnectRequest.inProgress).to.be.true;
- expect(initialStateForTest.sendingPatientDexcomConnectRequest.notification).to.be.null;
+ expect(initialStateForTest.sendingPatientDataProviderConnectRequest.inProgress).to.be.true;
+ expect(initialStateForTest.sendingPatientDataProviderConnectRequest.notification).to.be.null;
let state = reducer(initialStateForTest, action);
- expect(state.sendingPatientDexcomConnectRequest.inProgress).to.be.false;
- expect(state.sendingPatientDexcomConnectRequest.notification.type).to.equal('error');
- expect(state.sendingPatientDexcomConnectRequest.notification.message).to.equal(error.message);
+ expect(state.sendingPatientDataProviderConnectRequest.inProgress).to.be.false;
+ expect(state.sendingPatientDataProviderConnectRequest.notification.type).to.equal('error');
+ expect(state.sendingPatientDataProviderConnectRequest.notification.message).to.equal(error.message);
expect(mutationTracker.hasMutated(tracked)).to.be.false;
});
});
describe('success', () => {
- it('should set sendingPatientDexcomConnectRequest.completed to be true', () => {
- expect(initialState.sendingPatientDexcomConnectRequest.completed).to.be.null;
+ it('should set sendingPatientDataProviderConnectRequest.completed to be true', () => {
+ expect(initialState.sendingPatientDataProviderConnectRequest.completed).to.be.null;
- let successAction = actions.sync.sendPatientDexcomConnectRequestSuccess('foo');
+ let successAction = actions.sync.sendPatientDataProviderConnectRequestSuccess('foo');
let state = reducer(initialState, successAction);
- expect(state.sendingPatientDexcomConnectRequest.completed).to.be.true;
+ expect(state.sendingPatientDataProviderConnectRequest.completed).to.be.true;
expect(mutationTracker.hasMutated(tracked)).to.be.false;
});
- it('should set sendingPatientDexcomConnectRequest.inProgress to be false', () => {
+ it('should set sendingPatientDataProviderConnectRequest.inProgress to be false', () => {
let initialStateForTest = _.merge({}, initialState, {
- sendingPatientDexcomConnectRequest: { inProgress: true, notification: null },
+ sendingPatientDataProviderConnectRequest: { inProgress: true, notification: null },
});
let tracked = mutationTracker.trackObj(initialStateForTest);
- let action = actions.sync.sendPatientDexcomConnectRequestSuccess('strava', 'blah');
+ let action = actions.sync.sendPatientDataProviderConnectRequestSuccess('strava', 'blah');
- expect(initialStateForTest.sendingPatientDexcomConnectRequest.inProgress).to.be.true;
+ expect(initialStateForTest.sendingPatientDataProviderConnectRequest.inProgress).to.be.true;
let state = reducer(initialStateForTest, action);
- expect(state.sendingPatientDexcomConnectRequest.inProgress).to.be.false;
+ expect(state.sendingPatientDataProviderConnectRequest.inProgress).to.be.false;
expect(mutationTracker.hasMutated(tracked)).to.be.false;
});
});
diff --git a/yarn.lock b/yarn.lock
index 29d5ed21fd..7558f819e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5278,9 +5278,9 @@ __metadata:
languageName: node
linkType: hard
-"@tidepool/viz@npm:1.44.0-web-3179-dep-updates.3":
- version: 1.44.0-web-3179-dep-updates.3
- resolution: "@tidepool/viz@npm:1.44.0-web-3179-dep-updates.3"
+"@tidepool/viz@npm:1.45.0-web-3346-summary-accuracy.4":
+ version: 1.45.0-web-3346-summary-accuracy.4
+ resolution: "@tidepool/viz@npm:1.45.0-web-3346-summary-accuracy.4"
dependencies:
bluebird: 3.7.2
bows: 1.7.2
@@ -5340,7 +5340,7 @@ __metadata:
react-dom: 16.x
react-redux: 8.x
redux: 4.x
- checksum: 050cdbc7334512123f632afc0d620b595fdcb3bd481cefd9104713b5e1e0d42e20ef1ddf367eba717232becf223a31fe56b12203c6b5aa1b616d5f0b3fe5f1af
+ checksum: 18467d1d827507db3e579ab49f069ca6e26a3a2a81a5f4d6e95352aa397e17b8b112ea0c7df3d4043fc71df6eba17464bbcb44a3c257864a2334f27ff94713b9
languageName: node
linkType: hard
@@ -5582,33 +5582,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/eslint-scope@npm:^3.7.3":
- version: 3.7.7
- resolution: "@types/eslint-scope@npm:3.7.7"
- dependencies:
- "@types/eslint": "*"
- "@types/estree": "*"
- checksum: e2889a124aaab0b89af1bab5959847c5bec09809209255de0e63b9f54c629a94781daa04adb66bffcdd742f5e25a17614fb933965093c0eea64aacda4309380e
- languageName: node
- linkType: hard
-
-"@types/eslint@npm:*":
- version: 8.44.7
- resolution: "@types/eslint@npm:8.44.7"
- dependencies:
- "@types/estree": "*"
- "@types/json-schema": "*"
- checksum: 72a52f74477fbe7cc95ad290b491f51f0bc547cb7ea3672c68da3ffd3fb21ba86145bc36823a37d0a186caedeaee15b2d2a6b4c02c6c55819ff746053bd28310
- languageName: node
- linkType: hard
-
-"@types/estree@npm:*, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5":
- version: 1.0.5
- resolution: "@types/estree@npm:1.0.5"
- checksum: dd8b5bed28e6213b7acd0fb665a84e693554d850b0df423ac8076cc3ad5823a6bc26b0251d080bdc545af83179ede51dd3f6fa78cad2c46ed1f29624ddf3e41a
- languageName: node
- linkType: hard
-
"@types/estree@npm:^0.0.51":
version: 0.0.51
resolution: "@types/estree@npm:0.0.51"
@@ -5616,6 +5589,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/estree@npm:^1.0.5":
+ version: 1.0.6
+ resolution: "@types/estree@npm:1.0.6"
+ checksum: 8825d6e729e16445d9a1dd2fb1db2edc5ed400799064cd4d028150701031af012ba30d6d03fe9df40f4d7a437d0de6d2b256020152b7b09bde9f2e420afdffd9
+ languageName: node
+ linkType: hard
+
"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.33":
version: 4.17.41
resolution: "@types/express-serve-static-core@npm:4.17.41"
@@ -5730,7 +5710,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
+"@types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98
@@ -6064,248 +6044,154 @@ __metadata:
languageName: node
linkType: hard
-"@webassemblyjs/ast@npm:1.11.6, @webassemblyjs/ast@npm:^1.11.5":
- version: 1.11.6
- resolution: "@webassemblyjs/ast@npm:1.11.6"
- dependencies:
- "@webassemblyjs/helper-numbers": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- checksum: 38ef1b526ca47c210f30975b06df2faf1a8170b1636ce239fc5738fc231ce28389dd61ecedd1bacfc03cbe95b16d1af848c805652080cb60982836eb4ed2c6cf
- languageName: node
- linkType: hard
-
-"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/ast@npm:1.12.1"
+"@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.12.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/ast@npm:1.14.1"
dependencies:
- "@webassemblyjs/helper-numbers": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- checksum: 31bcc64147236bd7b1b6d29d1f419c1f5845c785e1e42dc9e3f8ca2e05a029e9393a271b84f3a5bff2a32d35f51ff59e2181a6e5f953fe88576acd6750506202
- languageName: node
- linkType: hard
-
-"@webassemblyjs/floating-point-hex-parser@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.11.6"
- checksum: 29b08758841fd8b299c7152eda36b9eb4921e9c584eb4594437b5cd90ed6b920523606eae7316175f89c20628da14326801090167cc7fbffc77af448ac84b7e2
+ "@webassemblyjs/helper-numbers": 1.13.2
+ "@webassemblyjs/helper-wasm-bytecode": 1.13.2
+ checksum: f9154ad9ea14f6f2374ebe918c221fd69a4d4514126a1acc6fa4966e8d27ab28cb550a5e6880032cf620e19640578658a7e5a55bd2aad1e3db4e9d598b8f2099
languageName: node
linkType: hard
-"@webassemblyjs/helper-api-error@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/helper-api-error@npm:1.11.6"
- checksum: e8563df85161096343008f9161adb138a6e8f3c2cc338d6a36011aa55eabb32f2fd138ffe63bc278d009ada001cc41d263dadd1c0be01be6c2ed99076103689f
+"@webassemblyjs/floating-point-hex-parser@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.13.2"
+ checksum: e866ec8433f4a70baa511df5e8f2ebcd6c24f4e2cc6274c7c5aabe2bcce3459ea4680e0f35d450e1f3602acf3913b6b8e4f15069c8cfd34ae8609fb9a7d01795
languageName: node
linkType: hard
-"@webassemblyjs/helper-buffer@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/helper-buffer@npm:1.11.6"
- checksum: b14d0573bf680d22b2522e8a341ec451fddd645d1f9c6bd9012ccb7e587a2973b86ab7b89fe91e1c79939ba96095f503af04369a3b356c8023c13a5893221644
+"@webassemblyjs/helper-api-error@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/helper-api-error@npm:1.13.2"
+ checksum: 48b5df7fd3095bb252f59a139fe2cbd999a62ac9b488123e9a0da3906ad8a2f2da7b2eb21d328c01a90da987380928706395c2897d1f3ed9e2125b6d75a920d0
languageName: node
linkType: hard
-"@webassemblyjs/helper-buffer@npm:1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/helper-buffer@npm:1.12.1"
- checksum: c3ffb723024130308db608e86e2bdccd4868bbb62dffb0a9a1530606496f79c87f8565bd8e02805ce64912b71f1a70ee5fb00307258b0c082c3abf961d097eca
+"@webassemblyjs/helper-buffer@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/helper-buffer@npm:1.14.1"
+ checksum: b611e981dfd6a797c3d8fc3a772de29a6e55033737c2c09c31bb66c613bdbb2d25f915df1dee62a602c6acc057ca71128432fa8c3e22a893e1219dc454f14ede
languageName: node
linkType: hard
-"@webassemblyjs/helper-numbers@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/helper-numbers@npm:1.11.6"
+"@webassemblyjs/helper-numbers@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/helper-numbers@npm:1.13.2"
dependencies:
- "@webassemblyjs/floating-point-hex-parser": 1.11.6
- "@webassemblyjs/helper-api-error": 1.11.6
+ "@webassemblyjs/floating-point-hex-parser": 1.13.2
+ "@webassemblyjs/helper-api-error": 1.13.2
"@xtuc/long": 4.2.2
- checksum: f4b562fa219f84368528339e0f8d273ad44e047a07641ffcaaec6f93e5b76fd86490a009aa91a294584e1436d74b0a01fa9fde45e333a4c657b58168b04da424
- languageName: node
- linkType: hard
-
-"@webassemblyjs/helper-wasm-bytecode@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.11.6"
- checksum: 3535ef4f1fba38de3475e383b3980f4bbf3de72bbb631c2b6584c7df45be4eccd62c6ff48b5edd3f1bcff275cfd605a37679ec199fc91fd0a7705d7f1e3972dc
+ checksum: 49e2c9bf9b66997e480f6b44d80f895b3cde4de52ac135921d28e144565edca6903a519f627f4089b5509de1d7f9e5023f0e1a94ff78a36c9e2eb30e7c18ffd2
languageName: node
linkType: hard
-"@webassemblyjs/helper-wasm-section@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/helper-wasm-section@npm:1.11.6"
- dependencies:
- "@webassemblyjs/ast": 1.11.6
- "@webassemblyjs/helper-buffer": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/wasm-gen": 1.11.6
- checksum: b2cf751bf4552b5b9999d27bbb7692d0aca75260140195cb58ea6374d7b9c2dc69b61e10b211a0e773f66209c3ddd612137ed66097e3684d7816f854997682e9
+"@webassemblyjs/helper-wasm-bytecode@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2"
+ checksum: 8e059e1c1f0294f4fc3df8e4eaff3c5ef6e2e1358f34ebc118eaf5070ed59e56ed7fc92b28be734ebde17c8d662d5d27e06ade686c282445135da083ae11c128
languageName: node
linkType: hard
-"@webassemblyjs/helper-wasm-section@npm:1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/helper-wasm-section@npm:1.12.1"
+"@webassemblyjs/helper-wasm-section@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/helper-wasm-section@npm:1.14.1"
dependencies:
- "@webassemblyjs/ast": 1.12.1
- "@webassemblyjs/helper-buffer": 1.12.1
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/wasm-gen": 1.12.1
- checksum: c19810cdd2c90ff574139b6d8c0dda254d42d168a9e5b3d353d1bc085f1d7164ccd1b3c05592a45a939c47f7e403dc8d03572bb686642f06a3d02932f6f0bc8f
+ "@webassemblyjs/ast": 1.14.1
+ "@webassemblyjs/helper-buffer": 1.14.1
+ "@webassemblyjs/helper-wasm-bytecode": 1.13.2
+ "@webassemblyjs/wasm-gen": 1.14.1
+ checksum: 0a08d454a63192cd66abf91b6f060ac4b466cef341262246e9dcc828dd4c8536195dea9b46a1244b1eac65b59b8b502164a771a190052a92ff0a0a2ded0f8f53
languageName: node
linkType: hard
-"@webassemblyjs/ieee754@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/ieee754@npm:1.11.6"
+"@webassemblyjs/ieee754@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/ieee754@npm:1.13.2"
dependencies:
"@xtuc/ieee754": ^1.2.0
- checksum: 13574b8e41f6ca39b700e292d7edf102577db5650fe8add7066a320aa4b7a7c09a5056feccac7a74eb68c10dea9546d4461412af351f13f6b24b5f32379b49de
+ checksum: d7e3520baa37a7309fa7db4d73d69fb869878853b1ebd4b168821bd03fcc4c0e1669c06231315b0039035d9a7a462e53de3ad982da4a426a4b0743b5888e8673
languageName: node
linkType: hard
-"@webassemblyjs/leb128@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/leb128@npm:1.11.6"
+"@webassemblyjs/leb128@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/leb128@npm:1.13.2"
dependencies:
"@xtuc/long": 4.2.2
- checksum: 7ea942dc9777d4b18a5ebfa3a937b30ae9e1d2ce1fee637583ed7f376334dd1d4274f813d2e250056cca803e0952def4b954913f1a3c9068bcd4ab4ee5143bf0
- languageName: node
- linkType: hard
-
-"@webassemblyjs/utf8@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/utf8@npm:1.11.6"
- checksum: 807fe5b5ce10c390cfdd93e0fb92abda8aebabb5199980681e7c3743ee3306a75729bcd1e56a3903980e96c885ee53ef901fcbaac8efdfa480f9c0dae1d08713
+ checksum: 64083507f7cff477a6d71a9e325d95665cea78ec8df99ca7c050e1cfbe300fbcf0842ca3dcf3b4fa55028350135588a4f879398d3dd2b6a8de9913ce7faf5333
languageName: node
linkType: hard
-"@webassemblyjs/wasm-edit@npm:^1.11.5":
- version: 1.11.6
- resolution: "@webassemblyjs/wasm-edit@npm:1.11.6"
- dependencies:
- "@webassemblyjs/ast": 1.11.6
- "@webassemblyjs/helper-buffer": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/helper-wasm-section": 1.11.6
- "@webassemblyjs/wasm-gen": 1.11.6
- "@webassemblyjs/wasm-opt": 1.11.6
- "@webassemblyjs/wasm-parser": 1.11.6
- "@webassemblyjs/wast-printer": 1.11.6
- checksum: 29ce75870496d6fad864d815ebb072395a8a3a04dc9c3f4e1ffdc63fc5fa58b1f34304a1117296d8240054cfdbc38aca88e71fb51483cf29ffab0a61ef27b481
+"@webassemblyjs/utf8@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/utf8@npm:1.13.2"
+ checksum: 95ec6052f30eefa8d50c9b2a3394d08b17d53a4aa52821451d41d774c126fa8f39b988fbf5bff56da86852a87c16d676e576775a4071e5e5ccf020cc85a4b281
languageName: node
linkType: hard
"@webassemblyjs/wasm-edit@npm:^1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/wasm-edit@npm:1.12.1"
- dependencies:
- "@webassemblyjs/ast": 1.12.1
- "@webassemblyjs/helper-buffer": 1.12.1
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/helper-wasm-section": 1.12.1
- "@webassemblyjs/wasm-gen": 1.12.1
- "@webassemblyjs/wasm-opt": 1.12.1
- "@webassemblyjs/wasm-parser": 1.12.1
- "@webassemblyjs/wast-printer": 1.12.1
- checksum: ae23642303f030af888d30c4ef37b08dfec7eab6851a9575a616e65d1219f880d9223913a39056dd654e49049d76e97555b285d1f7e56935047abf578cce0692
- languageName: node
- linkType: hard
-
-"@webassemblyjs/wasm-gen@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/wasm-gen@npm:1.11.6"
- dependencies:
- "@webassemblyjs/ast": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/ieee754": 1.11.6
- "@webassemblyjs/leb128": 1.11.6
- "@webassemblyjs/utf8": 1.11.6
- checksum: a645a2eecbea24833c3260a249704a7f554ef4a94c6000984728e94bb2bc9140a68dfd6fd21d5e0bbb09f6dfc98e083a45760a83ae0417b41a0196ff6d45a23a
- languageName: node
- linkType: hard
-
-"@webassemblyjs/wasm-gen@npm:1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/wasm-gen@npm:1.12.1"
- dependencies:
- "@webassemblyjs/ast": 1.12.1
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/ieee754": 1.11.6
- "@webassemblyjs/leb128": 1.11.6
- "@webassemblyjs/utf8": 1.11.6
- checksum: 5787626bb7f0b033044471ddd00ce0c9fe1ee4584e8b73e232051e3a4c99ba1a102700d75337151c8b6055bae77eefa4548960c610a5e4a504e356bd872138ff
- languageName: node
- linkType: hard
-
-"@webassemblyjs/wasm-opt@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/wasm-opt@npm:1.11.6"
- dependencies:
- "@webassemblyjs/ast": 1.11.6
- "@webassemblyjs/helper-buffer": 1.11.6
- "@webassemblyjs/wasm-gen": 1.11.6
- "@webassemblyjs/wasm-parser": 1.11.6
- checksum: b4557f195487f8e97336ddf79f7bef40d788239169aac707f6eaa2fa5fe243557c2d74e550a8e57f2788e70c7ae4e7d32f7be16101afe183d597b747a3bdd528
- languageName: node
- linkType: hard
-
-"@webassemblyjs/wasm-opt@npm:1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/wasm-opt@npm:1.12.1"
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-edit@npm:1.14.1"
dependencies:
- "@webassemblyjs/ast": 1.12.1
- "@webassemblyjs/helper-buffer": 1.12.1
- "@webassemblyjs/wasm-gen": 1.12.1
- "@webassemblyjs/wasm-parser": 1.12.1
- checksum: 0e8fa8a0645304a1e18ff40d3db5a2e9233ebaa169b19fcc651d6fc9fe2cac0ce092ddee927318015ae735d9cd9c5d97c0cafb6a51dcd2932ac73587b62df991
+ "@webassemblyjs/ast": 1.14.1
+ "@webassemblyjs/helper-buffer": 1.14.1
+ "@webassemblyjs/helper-wasm-bytecode": 1.13.2
+ "@webassemblyjs/helper-wasm-section": 1.14.1
+ "@webassemblyjs/wasm-gen": 1.14.1
+ "@webassemblyjs/wasm-opt": 1.14.1
+ "@webassemblyjs/wasm-parser": 1.14.1
+ "@webassemblyjs/wast-printer": 1.14.1
+ checksum: 9341c3146bb1b7863f03d6050c2a66990f20384ca137388047bbe1feffacb599e94fca7b7c18287d17e2449ffb4005fdc7f41f674a6975af9ad8522756f8ffff
languageName: node
linkType: hard
-"@webassemblyjs/wasm-parser@npm:1.11.6, @webassemblyjs/wasm-parser@npm:^1.11.5":
- version: 1.11.6
- resolution: "@webassemblyjs/wasm-parser@npm:1.11.6"
+"@webassemblyjs/wasm-gen@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-gen@npm:1.14.1"
dependencies:
- "@webassemblyjs/ast": 1.11.6
- "@webassemblyjs/helper-api-error": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/ieee754": 1.11.6
- "@webassemblyjs/leb128": 1.11.6
- "@webassemblyjs/utf8": 1.11.6
- checksum: 8200a8d77c15621724a23fdabe58d5571415cda98a7058f542e670ea965dd75499f5e34a48675184947c66f3df23adf55df060312e6d72d57908e3f049620d8a
+ "@webassemblyjs/ast": 1.14.1
+ "@webassemblyjs/helper-wasm-bytecode": 1.13.2
+ "@webassemblyjs/ieee754": 1.13.2
+ "@webassemblyjs/leb128": 1.13.2
+ "@webassemblyjs/utf8": 1.13.2
+ checksum: 401b12bec7431c4fc29d9414bbe40d3c6dc5be04d25a116657c42329f5481f0129f3b5834c293f26f0e42681ceac9157bf078ce9bdb6a7f78037c650373f98b2
languageName: node
linkType: hard
-"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/wasm-parser@npm:1.12.1"
+"@webassemblyjs/wasm-opt@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-opt@npm:1.14.1"
dependencies:
- "@webassemblyjs/ast": 1.12.1
- "@webassemblyjs/helper-api-error": 1.11.6
- "@webassemblyjs/helper-wasm-bytecode": 1.11.6
- "@webassemblyjs/ieee754": 1.11.6
- "@webassemblyjs/leb128": 1.11.6
- "@webassemblyjs/utf8": 1.11.6
- checksum: 176015de3551ac068cd4505d837414f258d9ade7442bd71efb1232fa26c9f6d7d4e11a5c816caeed389943f409af7ebff6899289a992d7a70343cb47009d21a8
+ "@webassemblyjs/ast": 1.14.1
+ "@webassemblyjs/helper-buffer": 1.14.1
+ "@webassemblyjs/wasm-gen": 1.14.1
+ "@webassemblyjs/wasm-parser": 1.14.1
+ checksum: 60c697a9e9129d8d23573856df0791ba33cea4a3bc2339044cae73128c0983802e5e50a42157b990eeafe1237eb8e7653db6de5f02b54a0ae7b81b02dcdf2ae9
languageName: node
linkType: hard
-"@webassemblyjs/wast-printer@npm:1.11.6":
- version: 1.11.6
- resolution: "@webassemblyjs/wast-printer@npm:1.11.6"
+"@webassemblyjs/wasm-parser@npm:1.14.1, @webassemblyjs/wasm-parser@npm:^1.12.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-parser@npm:1.14.1"
dependencies:
- "@webassemblyjs/ast": 1.11.6
- "@xtuc/long": 4.2.2
- checksum: d2fa6a4c427325ec81463e9c809aa6572af6d47f619f3091bf4c4a6fc34f1da3df7caddaac50b8e7a457f8784c62cd58c6311b6cb69b0162ccd8d4c072f79cf8
+ "@webassemblyjs/ast": 1.14.1
+ "@webassemblyjs/helper-api-error": 1.13.2
+ "@webassemblyjs/helper-wasm-bytecode": 1.13.2
+ "@webassemblyjs/ieee754": 1.13.2
+ "@webassemblyjs/leb128": 1.13.2
+ "@webassemblyjs/utf8": 1.13.2
+ checksum: 93f1fe2676da465b4e824419d9812a3d7218de4c3addd4e916c04bc86055fa134416c1b67e4b7cbde8d728c0dce2721d06cc0bfe7a7db7c093a0898009937405
languageName: node
linkType: hard
-"@webassemblyjs/wast-printer@npm:1.12.1":
- version: 1.12.1
- resolution: "@webassemblyjs/wast-printer@npm:1.12.1"
+"@webassemblyjs/wast-printer@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wast-printer@npm:1.14.1"
dependencies:
- "@webassemblyjs/ast": 1.12.1
+ "@webassemblyjs/ast": 1.14.1
"@xtuc/long": 4.2.2
- checksum: 2974b5dda8d769145ba0efd886ea94a601e61fb37114c14f9a9a7606afc23456799af652ac3052f284909bd42edc3665a76bc9b50f95f0794c053a8a1757b713
+ checksum: 517881a0554debe6945de719d100b2d8883a2d24ddf47552cdeda866341e2bb153cd824a864bc7e2a61190a4b66b18f9899907e0074e9e820d2912ac0789ea60
languageName: node
linkType: hard
@@ -6427,15 +6313,6 @@ __metadata:
languageName: node
linkType: hard
-"acorn-import-assertions@npm:^1.9.0":
- version: 1.9.0
- resolution: "acorn-import-assertions@npm:1.9.0"
- peerDependencies:
- acorn: ^8
- checksum: 944fb2659d0845c467066bdcda2e20c05abe3aaf11972116df457ce2627628a81764d800dd55031ba19de513ee0d43bb771bc679cc0eda66dc8b4fade143bc0c
- languageName: node
- linkType: hard
-
"acorn-import-attributes@npm:^1.9.5":
version: 1.9.5
resolution: "acorn-import-attributes@npm:1.9.5"
@@ -7374,7 +7251,7 @@ __metadata:
"@storybook/react": 7.5.0
"@storybook/react-webpack5": 7.5.0
"@testing-library/react-hooks": 8.0.1
- "@tidepool/viz": 1.44.0-web-3179-dep-updates.3
+ "@tidepool/viz": 1.45.0-web-3346-summary-accuracy.4
async: 2.6.4
autoprefixer: 10.4.16
babel-core: 7.0.0-bridge.0
@@ -7498,7 +7375,7 @@ __metadata:
terser-webpack-plugin: 5.3.9
theme-ui: 0.16.1
tideline: 1.30.0
- tidepool-platform-client: 0.61.0
+ tidepool-platform-client: 0.62.0-web-3272-patient-data-linking-after-creation.1
tidepool-standard-action: 0.1.1
ua-parser-js: 1.0.36
url-loader: 4.1.1
@@ -7731,7 +7608,7 @@ __metadata:
languageName: node
linkType: hard
-"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.21.9, browserslist@npm:^4.22.1":
+"browserslist@npm:^4.0.0, browserslist@npm:^4.21.10, browserslist@npm:^4.21.4, browserslist@npm:^4.21.9, browserslist@npm:^4.22.1":
version: 4.22.1
resolution: "browserslist@npm:4.22.1"
dependencies:
@@ -9968,16 +9845,6 @@ __metadata:
languageName: node
linkType: hard
-"enhanced-resolve@npm:^5.15.0":
- version: 5.15.0
- resolution: "enhanced-resolve@npm:5.15.0"
- dependencies:
- graceful-fs: ^4.2.4
- tapable: ^2.2.0
- checksum: fbd8cdc9263be71cc737aa8a7d6c57b43d6aa38f6cc75dde6fcd3598a130cc465f979d2f4d01bb3bf475acb43817749c79f8eef9be048683602ca91ab52e4f11
- languageName: node
- linkType: hard
-
"enhanced-resolve@npm:^5.17.1":
version: 5.17.1
resolution: "enhanced-resolve@npm:5.17.1"
@@ -20014,7 +19881,7 @@ __metadata:
languageName: node
linkType: hard
-"terser-webpack-plugin@npm:5.3.9, terser-webpack-plugin@npm:^5.3.1, terser-webpack-plugin@npm:^5.3.7":
+"terser-webpack-plugin@npm:5.3.9, terser-webpack-plugin@npm:^5.3.1":
version: 5.3.9
resolution: "terser-webpack-plugin@npm:5.3.9"
dependencies:
@@ -20087,8 +19954,8 @@ __metadata:
linkType: hard
"terser@npm:^5.26.0":
- version: 5.31.6
- resolution: "terser@npm:5.31.6"
+ version: 5.36.0
+ resolution: "terser@npm:5.36.0"
dependencies:
"@jridgewell/source-map": ^0.3.3
acorn: ^8.8.2
@@ -20096,7 +19963,7 @@ __metadata:
source-map-support: ~0.5.20
bin:
terser: bin/terser
- checksum: 60d3faf39c9ad7acc891e17888bbd206e0b777f442649cf49873a5fa317b8b8a17179a46970d884d5f93e8addde0206193ed1e2e4f1ccb1cafb167f7d1ddee96
+ checksum: 489afd31901a2b170f7766948a3aa0e25da0acb41e9e35bd9f9b4751dfa2fc846e485f6fb9d34f0839a96af77f675b5fbf0a20c9aa54e0b8d7c219cf0b55e508
languageName: node
linkType: hard
@@ -20185,15 +20052,15 @@ __metadata:
languageName: node
linkType: hard
-"tidepool-platform-client@npm:0.61.0":
- version: 0.61.0
- resolution: "tidepool-platform-client@npm:0.61.0"
+"tidepool-platform-client@npm:0.62.0-web-3272-patient-data-linking-after-creation.1":
+ version: 0.62.0-web-3272-patient-data-linking-after-creation.1
+ resolution: "tidepool-platform-client@npm:0.62.0-web-3272-patient-data-linking-after-creation.1"
dependencies:
async: 0.9.0
lodash: 4.17.21
superagent: 5.2.2
uuid: 3.1.0
- checksum: 96e221a65b7a003523e03d9ba357b0c1aa371b779ffd363c325ba015b893462955188662587b502193aaf269d6784577c8d24b8c764ba4ad759d4b0ba511c8ae
+ checksum: 4d418e359f3bf9ef01245a779506c39e565f82a219765c1ae522a78aed3f78a3da5d533d2e1d615ab40d35786edb9afdd4f731817bf0563cd4407d7aeb27f57c
languageName: node
linkType: hard
@@ -21628,7 +21495,7 @@ __metadata:
languageName: node
linkType: hard
-"watchpack@npm:^2.2.0, watchpack@npm:^2.4.0":
+"watchpack@npm:^2.2.0":
version: 2.4.0
resolution: "watchpack@npm:2.4.0"
dependencies:
@@ -21837,43 +21704,6 @@ __metadata:
languageName: node
linkType: hard
-"webpack@npm:5":
- version: 5.89.0
- resolution: "webpack@npm:5.89.0"
- dependencies:
- "@types/eslint-scope": ^3.7.3
- "@types/estree": ^1.0.0
- "@webassemblyjs/ast": ^1.11.5
- "@webassemblyjs/wasm-edit": ^1.11.5
- "@webassemblyjs/wasm-parser": ^1.11.5
- acorn: ^8.7.1
- acorn-import-assertions: ^1.9.0
- browserslist: ^4.14.5
- chrome-trace-event: ^1.0.2
- enhanced-resolve: ^5.15.0
- es-module-lexer: ^1.2.1
- eslint-scope: 5.1.1
- events: ^3.2.0
- glob-to-regexp: ^0.4.1
- graceful-fs: ^4.2.9
- json-parse-even-better-errors: ^2.3.1
- loader-runner: ^4.2.0
- mime-types: ^2.1.27
- neo-async: ^2.6.2
- schema-utils: ^3.2.0
- tapable: ^2.1.1
- terser-webpack-plugin: ^5.3.7
- watchpack: ^2.4.0
- webpack-sources: ^3.2.3
- peerDependenciesMeta:
- webpack-cli:
- optional: true
- bin:
- webpack: bin/webpack.js
- checksum: 43fe0dbc30e168a685ef5a86759d5016a705f6563b39a240aa00826a80637d4a3deeb8062e709d6a4b05c63e796278244c84b04174704dc4a37bedb0f565c5ed
- languageName: node
- linkType: hard
-
"webpack@npm:5.94.0":
version: 5.94.0
resolution: "webpack@npm:5.94.0"