Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-3275] patient data source management #1506

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0cf9ec4
Only show dexcom connection error banner
clintonium-119 Jan 22, 2025
c69d1e5
Merge branch 'WEB-3274-device-settings-data-sources' into WEB-3275-pa…
clintonium-119 Jan 23, 2025
0043d53
Merge branch 'WEB-3274-device-settings-data-sources' into WEB-3275-pa…
clintonium-119 Jan 24, 2025
524c92a
Remove data source management from patient profile page
clintonium-119 Jan 24, 2025
425c0b4
Remove old datasources component
clintonium-119 Jan 24, 2025
cd3c5e3
Add action to clear authorized data source
clintonium-119 Jan 24, 2025
72a600d
Fix broken tests
clintonium-119 Jan 24, 2025
dd75049
v1.84.0-web-3275-patient-data-source-management.1
clintonium-119 Jan 24, 2025
b09ac74
Add redux dataSources state to patient data for logged-in users as ex…
clintonium-119 Jan 24, 2025
66ae469
Omit fields that we don't ever intend to update on clinic patient upd…
clintonium-119 Jan 24, 2025
21221d6
Only fetch patient details after async data connection handler comple…
clintonium-119 Jan 27, 2025
dbd2033
Merge branch 'develop' into WEB-3275-patient-data-source-management
clintonium-119 Jan 27, 2025
da81632
Revert "Omit fields that we don't ever intend to update on clinic pat…
clintonium-119 Jan 27, 2025
e604675
Bump platform-client
clintonium-119 Jan 27, 2025
2f7dc9a
Handle data source disconnections
clintonium-119 Jan 27, 2025
ad4e58e
Update data connections modal copy for personal users
clintonium-119 Jan 27, 2025
188fac0
Clean up various unused data source props from patient profile
clintonium-119 Jan 27, 2025
6c49c83
v1.84.0-web-3275-patient-data-source-management.2
clintonium-119 Jan 28, 2025
b45bda1
Fix broken tests
clintonium-119 Jan 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/components/chart/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ const Settings = ({
const isClinicContext = !!selectedClinicId;
const [showDataConnectionsModal, setShowDataConnectionsModal] = useState(false);
const [showUploadOverlay, setShowUploadOverlay] = useState(false);
const patientData = clinicPatient || clinicPatientFromAccountInfo(patient);
const dataSources = useSelector(state => state.blip.dataSources);

const patientData = clinicPatient || {
...clinicPatientFromAccountInfo(patient),
dataSources,
};

const deviceSelectionPopupState = usePopupState({
variant: 'popover',
Expand Down
125 changes: 121 additions & 4 deletions app/components/datasources/DataConnections.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import get from 'lodash/get';
import includes from 'lodash/includes';
import intersection from 'lodash/intersection';
import keys from 'lodash/keys';
import last from 'lodash/last';
import map from 'lodash/map';
import max from 'lodash/max';
import min from 'lodash/min';
import noop from 'lodash/noop';
import reduce from 'lodash/reduce';
import { utils as vizUtils } from '@tidepool/viz';
Expand Down Expand Up @@ -115,7 +117,7 @@ export function getProviderHandlers(patient, selectedClinicId, provider) {
buttonText: t('Disconnect'),
buttonStyle: 'text',
action: actions.async.disconnectDataSource,
args: [api, id, dataSourceFilter],
args: [api, dataSourceFilter],
},
inviteSent: {
buttonDisabled: true,
Expand Down Expand Up @@ -338,13 +340,43 @@ export const DataConnections = (props) => {
const [activeHandler, setActiveHandler] = useState(null);
const dataConnectionProps = getDataConnectionProps(patient, isLoggedInUser, selectedClinicId, setActiveHandler);

const authorizedDataSource = useSelector((state) => state.blip.authorizedDataSource);
const [providerConnectionPopup, setProviderConnectionPopup] = useState(null);

const openProviderConnectionPopup = useCallback((url, displayName) => {
const popupWidth = min([window.innerWidth * .85, 1080]);
const popupHeight = min([window.innerHeight * .85, 840]);
const popupLeft = window.screenX + (window.outerWidth - popupWidth) / 2;
const popupTop = window.screenY + (window.outerHeight - popupHeight) / 2;

const popupOptions = [
'toolbar=no',
'location=no',
'directories=no',
'status=no',
'menubar=no',
'scrollbars=yes',
'resizable=yes',
'copyhistory=no',
`width=${popupWidth}`,
`height=${popupHeight}`,
`left=${popupLeft}`,
`top=${popupTop}`,
];

const popup = window.open(url, `Connect ${displayName} to Tidepool`, popupOptions.join(','));
setProviderConnectionPopup(popup);
}, []);

const {
sendingPatientDataProviderConnectRequest,
updatingClinicPatient,
disconnectingDataSource,
} = useSelector((state) => state.blip.working);

const previousSendingPatientDataProviderConnectRequest = usePrevious(sendingPatientDataProviderConnectRequest);
const previousUpdatingClinicPatient = usePrevious(updatingClinicPatient);
const previousDisconnectingDataSource = usePrevious(disconnectingDataSource);

const fetchPatientDetails = useCallback(() => {
dispatch(actions.async.fetchPatientFromClinic(api, selectedClinicId, patient?.id));
Expand Down Expand Up @@ -465,8 +497,13 @@ export const DataConnections = (props) => {
setShowPatientEmailModal(false);
setShowResendDataSourceConnectRequest(false);
setActiveHandler(null);
fetchPatientDetails();
}, [fetchPatientDetails]);

if (selectedClinicId) {
fetchPatientDetails();
} else {
dispatch(actions.async.fetchDataSources(api));
}
}, [fetchPatientDetails, selectedClinicId, dispatch]);

useEffect(() => {
if(activeHandler?.action && !activeHandler?.inProgress) {
Expand Down Expand Up @@ -520,6 +557,84 @@ export const DataConnections = (props) => {
patient?.email
]);

useEffect(() => {
handleAsyncResult({ ...disconnectingDataSource, prevInProgress: previousDisconnectingDataSource?.inProgress }, t('{{ providerDisplayName }} connection has been disconnected.', {
providerDisplayName: providers[activeHandler?.providerName]?.displayName,
}), handleActiveHandlerComplete);
}, [
disconnectingDataSource,
previousDisconnectingDataSource?.inProgress,
handleAsyncResult,
handleActiveHandlerComplete,
activeHandler?.providerName,
]);

useEffect(() => {
if (authorizedDataSource?.id) {
const provider = find(providers, { id: authorizedDataSource.id});
if (provider) openProviderConnectionPopup(authorizedDataSource?.url, provider?.displayName);
}
}, [authorizedDataSource, openProviderConnectionPopup]);

useEffect(() => {
let timer;
if (!providerConnectionPopup || providerConnectionPopup.closed) {
return;
}

timer = setInterval(() => {
if (!providerConnectionPopup || providerConnectionPopup.closed) {
timer && clearInterval(timer);
dispatch(actions.sync.clearAuthorizedDataSource());
setProviderConnectionPopup(null);
handleActiveHandlerComplete();
return;
}

try {
const currentUrl = providerConnectionPopup.location.href;
const currentPath = providerConnectionPopup.location.pathname;

if (!currentUrl) return;

if (currentUrl.indexOf(authorizedDataSource?.id) !== -1) {
providerConnectionPopup.close();
const status = last(currentPath.split('/'));

const toastMessages = {
authorized: t('Connection Authorized. Thank you for connecting!'),
declined: t('Connection Declined. You can always decide to connect at a later time.'),
error: t('Connection Authorization Error. Please try again.'),
};

const toastVariants = {
authorized: 'success',
declined: 'info',
error: 'danger',
};

setToast({
message: toastMessages[status],
variant: toastVariants[status],
});
}
} catch (e) {
// The above try block will fail while the user is navigated to an external site, due to
// trying to access the currentUrl being a CORS violation.
// Once they complete the authorization flow, they will be redirected to our
// /oauth/[providerName]/[status] route where we can react to the results and then close the
// modal. So, we just return while this loop runs in the meantime.
return;
}
}, 500);
}, [
dispatch,
providerConnectionPopup,
authorizedDataSource?.id,
handleActiveHandlerComplete,
setToast,
]);

return (
<>
<Box id="data-connections" {...themeProps}>
Expand Down Expand Up @@ -578,7 +693,9 @@ const userDataSourceShape = {

DataConnections.propTypes = {
...BoxProps,
patient: PropTypes.oneOf([PropTypes.shape(clinicPatientDataSourceShape), PropTypes.shape(userDataSourceShape)]),
patient: PropTypes.shape({
dataSources: PropTypes.oneOf([PropTypes.shape(clinicPatientDataSourceShape), PropTypes.shape(userDataSourceShape)])
}),
shownProviders: PropTypes.arrayOf(PropTypes.oneOf(activeProviders)),
trackMetric: PropTypes.func.isRequired,
};
Expand Down
75 changes: 56 additions & 19 deletions app/components/datasources/DataConnectionsModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ export const DataConnectionsModal = (props) => {
const { set: setToast } = useToasts();
const selectedClinicId = useSelector((state) => state.blip.selectedClinicId);
const { updatingClinicPatient } = useSelector((state) => state.blip.working);
const dataSources = useSelector((state) => state.blip.dataSources);
const previousUpdatingClinicPatient = usePrevious(updatingClinicPatient);
const patientData = (patient?.profile) ? clinicPatientFromAccountInfo(patient) : patient;
const [showPatientEmailModal, setShowPatientEmailModal] = useState(false);

const patientData = (patient?.profile) ? {
...clinicPatientFromAccountInfo(patient),
dataSources,
} : patient;

const [showPatientEmailModal, setShowPatientEmailModal] = useState(false);
const [processingEmailUpdate, setProcessingEmailUpdate] = useState(false);
const [patientEmailFormContext, setPatientEmailFormContext] = useState();
const dispatch = useDispatch();
Expand Down Expand Up @@ -114,6 +120,14 @@ export const DataConnectionsModal = (props) => {
setToast,
]);

const dataSourcesText = selectedClinicId
? t('Invite patients to authorize syncing from these accounts. Only available in the US at this time.')
: t('When you connect an account, data can flow into Tidepool without any extra effort. Only available in the US at this time.');

const learnMoreText = selectedClinicId
? t('Learn more.')
: t('Learn more here.');

return (
<>
<Dialog
Expand All @@ -129,12 +143,12 @@ export const DataConnectionsModal = (props) => {
</DialogTitle>

<DialogContent>
<PatientDetails mb={3} patient={patientData} />
{!!selectedClinicId && <PatientDetails mb={3} patient={patientData} />}
<Subheading sx={{ fontWeight: 'bold'}}>{t('Connect a Device Account')}</Subheading>

<Box mb={3}>
<Body1 sx={{ fontWeight: 'medium'}}>
{t('Invite patients to authorize syncing from these accounts. Only available in the US at this time.')}&nbsp;
{dataSourcesText}&nbsp;
<Link
id="data-connections-restrictions-link"
href={URL_TIDEPOOL_EXTERNAL_DATA_CONNECTIONS}
Expand All @@ -144,7 +158,7 @@ export const DataConnectionsModal = (props) => {
fontSize: 1,
fontWeight: 'medium',
}}
>{t('Learn more.')}</Link>
>{learnMoreText}</Link>
</Body1>

{patientData?.email && patient?.permissions?.custodian && (
Expand All @@ -168,20 +182,43 @@ export const DataConnectionsModal = (props) => {
<DataConnections mb={4} patient={patientData} shownProviders={shownProviders} trackMetric={trackMetric} />
<Divider mb={3} />

<Body1 sx={{ fontWeight: 'medium'}}>
{t('Have other devices with data to view? Tidepool supports over 85 devices. To add data from a device directly, search for this patient in')}&nbsp;
<Link
id="data-connections-restrictions-link"
href={URL_UPLOADER_DOWNLOAD_PAGE}
target="_blank"
rel="noreferrer noopener"
sx={{
fontSize: 1,
fontWeight: 'medium',
}}
>{t('Tidepool Uploader')}</Link>,&nbsp;
{t('select the devices, and upload.')}&nbsp;
</Body1>
{!!selectedClinicId && (
<Body1 sx={{ fontWeight: 'medium'}}>
{t('Have other devices with data to view? Tidepool supports over 85 devices. To add data from a device directly, search for this patient in')}&nbsp;

<Link
id="data-connections-restrictions-link"
href={URL_UPLOADER_DOWNLOAD_PAGE}
target="_blank"
rel="noreferrer noopener"
sx={{
fontSize: 1,
fontWeight: 'medium',
}}
>{t('Tidepool Uploader')}</Link>,&nbsp;

{t('select the devices, and upload.')}&nbsp;
</Body1>
)}

{!selectedClinicId && (
<Body1 sx={{ fontWeight: 'medium'}}>
{t('Don’t have any of the accounts above? Tidepool supports over 85 devices. Open')}&nbsp;

<Link
id="data-connections-restrictions-link"
href={URL_UPLOADER_DOWNLOAD_PAGE}
target="_blank"
rel="noreferrer noopener"
sx={{
fontSize: 1,
fontWeight: 'medium',
}}
>{t('Tidepool Uploader')}</Link>,&nbsp;

{t('select your devices, and upload directly.')}&nbsp;
</Body1>
)}

{showPatientEmailModal && <PatientEmailModal
action="edit"
Expand Down
Loading