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-3352] - TIDE Drawer Navigation Bugs #1500

Open
wants to merge 14 commits into
base: WEB-3397-mobile-views
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
23 changes: 12 additions & 11 deletions app/components/navpatientheader/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useHistory, useLocation } from 'react-router-dom';
import { Box, Flex } from 'theme-ui';
import _ from 'lodash';
import launchCustomProtocol from 'custom-protocol-detection';
Expand Down Expand Up @@ -35,44 +35,45 @@ const NavPatientHeader = ({
user,
permsOfLoggedInUser,
trackMetric,
clinicFlowActive,
selectedClinicId,
query,
clinicFlowActive,
selectedClinicId,
}) => {
const history = useHistory();
const { search } = useLocation();
const [initialSearchParams] = useState(new URLSearchParams(search));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By storing in state here, I am just caching the value url searchParams at the first render, so that if the route changes, the back button has a reference to what the search params were on component mount

Screen.Recording.2025-01-21.at.11.08.10.mov

const [isUploadOverlayOpen, setIsUploadOverlayOpen] = useState(false);

if (!patient?.profile?.patient) return null;

const { patientListLink } = getPatientListLink(clinicFlowActive, selectedClinicId, query, patient.userid);
const patientListLink = getPatientListLink(clinicFlowActive, selectedClinicId, initialSearchParams, patient.userid);
const { canUpload, canShare } = getPermissions(patient, permsOfLoggedInUser);
const { mrn, birthday, name } = getDemographicInfo(patient, clinicPatient);

const handleBack = () => {
trackMetric('Clinic - View patient list', { clinicId: selectedClinicId, source: 'Patient data' });
history.push(patientListLink);
}
};

const handleUpload = () => {
trackMetric('Clicked Navbar Upload Data');
setIsUploadOverlayOpen(true);
launchCustomProtocol('tidepoolupload://open');
}
};

const handleViewData = () => {
trackMetric('Clicked Navbar View Data');
history.push(`/patients/${patient.userid}/data`);
}
};

const handleViewProfile = () => {
trackMetric('Clicked Navbar Name');
history.push(`/patients/${patient.userid}/profile`);
}
};

const handleShare = () => {
trackMetric('Clicked Navbar Share Data');
history.push(`/patients/${patient.userid}/share`);
}
};

return (
<div className="nav-patient-header">
Expand Down Expand Up @@ -104,6 +105,6 @@ const NavPatientHeader = ({
}
</div>
);
}
};

export default NavPatientHeader;
22 changes: 13 additions & 9 deletions app/components/navpatientheader/navPatientHeaderHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ export const getPermissions = (patient, permsOfLoggedInUser) => {
const canShare = _.isEmpty(permissions) === false && permissions.root;

return { canUpload, canShare };
}
};

export const getPatientListLink = (clinicFlowActive, selectedClinicId, query, patientId) => {
let patientListLink = clinicFlowActive && selectedClinicId ? '/clinic-workspace/patients' : '/patients';
if (query?.dashboard) {
patientListLink = `/dashboard/${query.dashboard}?drawerPatientId=${patientId}`;
export const getPatientListLink = (clinicFlowActive, selectedClinicId, initialSearchParams, patientId) => {
const dashboard = initialSearchParams.get('dashboard');

if (dashboard) {
return `/dashboard/${dashboard}?drawerPatientId=${patientId}`;
};

if (clinicFlowActive && selectedClinicId) {
return '/clinic-workspace/patients';
}

return { patientListLink };
}
return '/patients';
};

export const getDemographicInfo = (patient, clinicPatient) => {
const combinedPatient = personUtils.combinedAccountAndClinicPatient(patient, clinicPatient);
Expand All @@ -28,4 +32,4 @@ export const getDemographicInfo = (patient, clinicPatient) => {
const mrn = clinicPatient?.mrn;

return { name, birthday, mrn };
}
};
2 changes: 0 additions & 2 deletions app/pages/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ export class AppComponent extends React.Component {
context: { trackMetric },
clinicFlowActive,
selectedClinicId,
query,
} = this.props;

if (!this.isPatientVisibleInNavbar()) return null; // only show on pages with a patient of focus
Expand All @@ -367,7 +366,6 @@ export class AppComponent extends React.Component {
permsOfLoggedInUser={permsOfLoggedInUser}
clinicFlowActive={clinicFlowActive}
selectedClinicId={selectedClinicId}
query={query}
/>
);
}
Expand Down
8 changes: 7 additions & 1 deletion app/pages/dashboard/PatientDrawer/PatientDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styled from '@emotion/styled';
import { makeStyles } from '@material-ui/core/styles';
import CloseRoundedIcon from '@material-ui/icons/CloseRounded';
import { Box } from 'theme-ui';
import { useFlags } from 'launchdarkly-react-client-sdk';

import Content from './Content';
import MenuBar from './MenuBar';
Expand Down Expand Up @@ -35,6 +36,8 @@ const useStyles = makeStyles({
const DESKTOP_DRAWER_WIDTH = '1000px';
const DRAWER_CLOSE_BUTTON_GAP = '70px';

export const isValidAgpPeriod = period => ['7d', '14d', '30d'].includes(period);

const getAgpPeriodInDays = (period) => {
switch(period) {
case '1d': // minimum 7-day AGP period, so return 7
Expand All @@ -47,8 +50,11 @@ const getAgpPeriodInDays = (period) => {

const PatientDrawer = ({ patientId, onClose, api, trackMetric, period }) => {
const classes = useStyles();
const { showTideDashboardPatientDrawer } = useFlags();

if (!showTideDashboardPatientDrawer) return null;

const isOpen = !!patientId && !!period;
const isOpen = !!patientId && isValidAgpPeriod(period);

const agpPeriodInDays = getAgpPeriodInDays(period);

Expand Down
4 changes: 3 additions & 1 deletion app/pages/dashboard/PatientDrawer/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import PatientDrawer from './PatientDrawer';
import PatientDrawer, { isValidAgpPeriod } from './PatientDrawer';

export { isValidAgpPeriod };

export default PatientDrawer;
20 changes: 14 additions & 6 deletions app/pages/dashboard/TideDashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import PopoverMenu from '../../components/elements/PopoverMenu';
import RadioGroup from '../../components/elements/RadioGroup';
import DeltaBar from '../../components/elements/DeltaBar';
import Pill from '../../components/elements/Pill';
import PatientDrawer from './PatientDrawer';
import PatientDrawer, { isValidAgpPeriod } from './PatientDrawer';
import utils from '../../core/utils';

import {
Expand Down Expand Up @@ -909,15 +909,23 @@ export const TideDashboard = (props) => {

const drawerPatientId = new URLSearchParams(location.search).get('drawerPatientId') || null;

// Failsafe to ensure blip.pdf is always cleared out after drawer is closed
// Patient Drawer effects
useEffect(() => {
const isOpen = !!drawerPatientId;
// If invalid period for AGP (ie. 1 day), clear drawerPatientId from searchParams
if (!!drawerPatientId && !!config?.period && !isValidAgpPeriod(config.period)) {
const { search, pathname } = location;

if (!isOpen && !isEmpty(pdf)) {
const params = new URLSearchParams(search);
params.delete('drawerPatientId');
history.replace({ pathname, search: params.toString() });
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really am not a fan of doing this kind of thing in side effects like this. I worked on a few of different solutions and this ended up being the best of the evils.

The main challenge is the question of how to make the drawer initialize in an open or closed state but without requiring the originating page to have contextual knowledge about the TIDE dashboard.


// Failsafe to ensure blip.pdf is always cleared out after drawer is closed
if (!drawerPatientId && !isEmpty(pdf)) {
dispatch(actions.worker.removeGeneratedPDFS());
dispatch(actions.worker.dataWorkerRemoveDataRequest(null, drawerPatientId));
}
}, [drawerPatientId, pdf]);
}, [drawerPatientId, pdf, config, location, history]);

const handleEditPatientConfirm = useCallback(() => {
trackMetric('Clinic - Edit patient confirmed', { clinicId: selectedClinicId });
Expand Down Expand Up @@ -1248,7 +1256,7 @@ export const TideDashboard = (props) => {
{showEditPatientDialog && renderEditPatientDialog()}

<PatientDrawer
patientId={showTideDashboardPatientDrawer ? drawerPatientId : null}
patientId={drawerPatientId}
onClose={handleClosePatientDrawer}
api={api}
trackMetric={trackMetric}
Expand Down
61 changes: 43 additions & 18 deletions test/unit/components/navpatientheader/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const patientProps = {
profile: {
fullName: 'Vasyl Lomachenko',
patient: { birthday: '2010-10-20', mrn: '567890' },
}
},
};

const clinicPatientProps = {
Expand All @@ -34,13 +34,19 @@ const clinicPatientProps = {
};

describe('NavPatientHeader', () => {
const mockHistory = { push: sinon.stub() }
const mockHistory = { push: sinon.stub() };
const mockUseLocation = sinon.stub().returns({ pathname: '/patients/1234/data', search: '' });
const mockTrackMetric = sinon.stub();
const mockLaunchCustomProtocol = sinon.stub();

NavPatientHeader.__Rewire__('useHistory', sinon.stub().returns(mockHistory));
NavPatientHeader.__Rewire__('useLocation', mockUseLocation);
NavPatientHeader.__Rewire__('launchCustomProtocol', mockLaunchCustomProtocol);

beforeEach(() => {
mockUseLocation.returns({ pathname: '/patients/1234/data', search: '' });
});

afterEach(() => {
mockTrackMetric.reset();
mockHistory.push.reset();
Expand Down Expand Up @@ -79,7 +85,7 @@ describe('NavPatientHeader', () => {

expect(wrapper.find('button#navPatientHeader_shareButton').exists()).to.be.false;
expect(wrapper.find('button#navPatientHeader_uploadButton').exists()).to.be.false;
})
});
});

context('personal user with root permissions', () => {
Expand Down Expand Up @@ -112,7 +118,7 @@ describe('NavPatientHeader', () => {
expect(wrapper.find('button#navPatientHeader_profileButton').exists()).to.be.true;
expect(wrapper.find('button#navPatientHeader_shareButton').exists()).to.be.true;
expect(wrapper.find('button#navPatientHeader_uploadButton').exists()).to.be.true;
})
});
});

context('clinician user without upload permissions', () => {
Expand Down Expand Up @@ -146,7 +152,7 @@ describe('NavPatientHeader', () => {
expect(wrapper.find('button#navPatientHeader_profileButton').exists()).to.be.true;
expect(wrapper.find('button#navPatientHeader_shareButton').exists()).to.be.false;
expect(wrapper.find('button#navPatientHeader_uploadButton').exists()).to.be.false;
})
});
});

context('clinician user with upload permissions', () => {
Expand All @@ -155,7 +161,7 @@ describe('NavPatientHeader', () => {
trackMetric: mockTrackMetric,
patient: { ...patientProps, permissions: { root: {} } },
clinicPatient: { ...clinicPatientProps },
permsOfLoggedInUser: { upload: {} }
permsOfLoggedInUser: { upload: {} },
};

const wrapper = mount(
Expand All @@ -180,7 +186,7 @@ describe('NavPatientHeader', () => {
expect(wrapper.find('button#navPatientHeader_profileButton').exists()).to.be.true;
expect(wrapper.find('button#navPatientHeader_shareButton').exists()).to.be.false;
expect(wrapper.find('button#navPatientHeader_uploadButton').exists()).to.be.true;
})
});
});
});

Expand All @@ -200,28 +206,28 @@ describe('NavPatientHeader', () => {
<NavPatientHeader {...props} />
</BrowserRouter>
);
})
});

it('View button links to correct page', () => {
wrapper.find('button#navPatientHeader_viewDataButton').simulate('click');

expect(mockTrackMetric.calledOnceWithExactly('Clicked Navbar View Data')).to.be.true;
expect(mockHistory.push.calledOnceWithExactly('/patients/1234/data')).to.be.true;
})
});

it('Profile button links to correct page', () => {
wrapper.find('button#navPatientHeader_profileButton').simulate('click');

expect(mockTrackMetric.calledOnceWithExactly('Clicked Navbar Name')).to.be.true;
expect(mockHistory.push.calledOnceWithExactly('/patients/1234/profile')).to.be.true;
})
});

it('Share button links to correct page', () => {
wrapper.find('button#navPatientHeader_shareButton').simulate('click');

expect(mockTrackMetric.calledOnceWithExactly('Clicked Navbar Share Data')).to.be.true;
expect(mockHistory.push.calledOnceWithExactly('/patients/1234/share')).to.be.true;
})
});

it('Upload Button opens the upload dialog', () => {
expect(wrapper.find('.UploadLaunchOverlay').exists()).to.be.false;
Expand Down Expand Up @@ -250,20 +256,20 @@ describe('NavPatientHeader', () => {
<NavPatientHeader {...props} />
</BrowserRouter>
);
})
});

it('View button links to correct page', () => {
wrapper.find('button#navPatientHeader_viewDataButton').simulate('click');

expect(mockTrackMetric.calledOnceWithExactly('Clicked Navbar View Data')).to.be.true;
expect(mockHistory.push.calledOnceWithExactly('/patients/1234/data')).to.be.true;
})
});

it('Profile button links to correct page', () => {
wrapper.find('button#navPatientHeader_profileButton').simulate('click');
expect(mockTrackMetric.calledOnceWithExactly('Clicked Navbar Name')).to.be.true;
expect(mockHistory.push.calledOnceWithExactly('/patients/1234/profile')).to.be.true;
})
});

it('Upload Button opens the upload dialog', () => {
expect(wrapper.find('.UploadLaunchOverlay').exists()).to.be.false;
Expand All @@ -289,14 +295,14 @@ describe('NavPatientHeader', () => {
wrapper = mount(<BrowserRouter><NavPatientHeader {...clinicianUserProps} currentPage="/patients/abc123/data" /></BrowserRouter>);
wrapper.find('button#navPatientHeader_backButton').simulate('click');
expect(mockHistory.push.calledOnceWithExactly('/patients')).to.be.true;
})
});

it('should render on profile page', () => {
wrapper = mount(<BrowserRouter><NavPatientHeader {...clinicianUserProps} currentPage="/patients/abc123/profile" /></BrowserRouter>);
wrapper.find('button#navPatientHeader_backButton').simulate('click');
expect(mockHistory.push.calledOnceWithExactly('/patients')).to.be.true;
})
})
});
});

describe('viewing patient data or profile views as a clinic clinician', () => {
const clinicClinicianProps = {
Expand Down Expand Up @@ -328,6 +334,25 @@ describe('NavPatientHeader', () => {
});
});

describe('viewing patient or profile views and user originates from TIDE Dashboard', () => {
const clinicClinicianProps = {
trackMetric: mockTrackMetric,
patient: { ...patientProps, permissions: { } },
clinicFlowActive: true,
user: { isClinicMember: true },
selectedClinicId: 'clinic123',
};

it('should redirect to the correct path on TIDE dashboard', () => {
mockUseLocation.returns({ pathname: '/patients/abc1234/data', search: '?dashboard=tide' });

wrapper = mount(<BrowserRouter><NavPatientHeader {...clinicClinicianProps} currentPage="/patients/abc123/data" /></BrowserRouter>);
wrapper.find('button#navPatientHeader_backButton').simulate('click');

expect(mockHistory.push.calledOnceWithExactly('/dashboard/tide?drawerPatientId=1234')).to.be.true;
});
});

describe('viewing the TIDE dashboard view as a clinic clinician', () => {
it('should render a patient list link', () => {
const clinicClinicianProps = {
Expand All @@ -343,6 +368,6 @@ describe('NavPatientHeader', () => {
wrapper.find('button#navPatientHeader_backButton').simulate('click');
expect(mockHistory.push.calledOnceWithExactly('/clinic-workspace/patients')).to.be.true;
});
})
});
});
});