Skip to content

Commit 494bafa

Browse files
authored
Merge pull request #4695 from kalletlak/fix-10323
Enhancements to patient page
2 parents 375c2c7 + e979cf9 commit 494bafa

File tree

4 files changed

+182
-66
lines changed

4 files changed

+182
-66
lines changed

src/pages/patientView/PatientViewPage.tsx

Lines changed: 181 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import _ from 'lodash';
33
import { DiscreteCopyNumberData, ResourceData } from 'cbioportal-ts-api-client';
44
import { PaginationControls } from '../../shared/components/paginationControls/PaginationControls';
55
import { IColumnVisibilityDef } from 'shared/components/columnVisibilityControls/ColumnVisibilityControls';
6-
import { toggleColumnVisibility } from 'cbioportal-frontend-commons';
6+
import {
7+
DefaultTooltip,
8+
toggleColumnVisibility,
9+
} from 'cbioportal-frontend-commons';
710
import {
811
PatientViewPageStore,
912
buildCohortIdsFromNavCaseIds,
1013
} from './clinicalInformation/PatientViewPageStore';
1114
import { inject, observer } from 'mobx-react';
12-
import { action, computed, observable, makeObservable } from 'mobx';
15+
import { action, computed, observable, makeObservable, toJS } from 'mobx';
1316
import { default as PatientViewMutationTable } from './mutation/PatientViewMutationTable';
1417
import { MSKTab } from '../../shared/components/MSKTabs/MSKTabs';
1518
import ValidationAlert from 'shared/components/ValidationAlert';
@@ -18,7 +21,11 @@ import PatientViewCnaDataStore from './copyNumberAlterations/PatientViewCnaDataS
1821

1922
import './patient.scss';
2023

21-
import { getWholeSlideViewerUrl } from '../../shared/api/urls';
24+
import {
25+
buildCBioPortalPageUrl,
26+
getPatientViewUrl,
27+
getWholeSlideViewerUrl,
28+
} from '../../shared/api/urls';
2229
import { PageLayout } from '../../shared/components/PageLayout/PageLayout';
2330
import Helmet from 'react-helmet';
2431
import { getServerConfig } from '../../config/config';
@@ -55,11 +62,13 @@ import { prepareCustomTabConfigurations } from 'shared/lib/customTabs/customTabH
5562
import setWindowVariable from 'shared/lib/setWindowVariable';
5663
import { getNavCaseIdsCache } from 'shared/lib/handleLongUrls';
5764
import PatientViewPageHeader from 'pages/patientView/PatientViewPageHeader';
65+
import { MAX_URL_LENGTH } from 'pages/studyView/studyPageHeader/ActionButtons';
5866

5967
export interface IPatientViewPageProps {
6068
routing: any;
6169
appStore: AppStore;
6270
cohortIds?: string[];
71+
onCohortIdsUpdate: (ids: string[]) => void;
6372
}
6473

6574
export interface IGenePanelModal {
@@ -87,15 +96,14 @@ export default class PatientViewPage extends React.Component<
8796
IPatientViewPageProps,
8897
{}
8998
> {
90-
cohortIds: string[] | undefined;
99+
@observable cohortIds: string[] | undefined;
91100

92101
constructor(props: IPatientViewPageProps) {
93102
super(props);
94-
103+
makeObservable(this);
95104
const postData = getBrowserWindow().clientPostedData;
96105

97106
const urlData = getNavCaseIdsCache();
98-
99107
if (postData && postData.navCaseIds) {
100108
this.cohortIds = buildCohortIdsFromNavCaseIds(postData.navCaseIds);
101109
getBrowserWindow().clientPostedData = null;
@@ -104,12 +112,18 @@ export default class PatientViewPage extends React.Component<
104112
}
105113
}
106114

115+
@action.bound
116+
updateCohortIds(newCohortIds: string[]) {
117+
this.cohortIds = newCohortIds;
118+
}
119+
107120
render() {
108121
return (
109122
<PatientViewPageInner
110123
key={`${this.props.routing.query.caseId}-${this.props.routing.query.studyId}-${this.props.routing.query.sampleId}`}
111124
{...this.props}
112125
cohortIds={this.cohortIds}
126+
onCohortIdsUpdate={this.updateCohortIds}
113127
/>
114128
);
115129
}
@@ -544,6 +558,70 @@ export class PatientViewPageInner extends React.Component<
544558
return this.patientViewPageStore;
545559
}
546560

561+
@action.bound
562+
private handleReturnToStudyView() {
563+
const patientIdentifiers = this.pageStore.patientIdsInCohort.map(p => {
564+
const patientIdParts = p.split(':');
565+
return {
566+
patientId: patientIdParts[1],
567+
studyId: patientIdParts[0],
568+
};
569+
});
570+
const queriedStudies: Set<string> = new Set(
571+
patientIdentifiers.map(p => p.studyId)
572+
);
573+
574+
// We need to do this because of url length limits. We post the data to the new window once it is opened.
575+
const studyPage = window.open(
576+
buildCBioPortalPageUrl(`study`, {
577+
id: Array.from(queriedStudies).join(','),
578+
}),
579+
'_blank'
580+
);
581+
if (patientIdentifiers.length > 0) {
582+
(studyPage as any).studyPageFilter = `filterJson=${JSON.stringify({
583+
patientIdentifiers,
584+
})}`;
585+
}
586+
}
587+
588+
@action.bound
589+
private handleDeletePatient(deleteAtIndex: number) {
590+
var newCohortIdList = toJS(this.pageStore.patientIdsInCohort);
591+
newCohortIdList.splice(deleteAtIndex, 1);
592+
let currentIndex =
593+
deleteAtIndex > newCohortIdList.length - 1
594+
? deleteAtIndex - 1
595+
: deleteAtIndex;
596+
597+
let navCaseIds = newCohortIdList.map(p => {
598+
const patientIdParts = p.split(':');
599+
return {
600+
patientId: patientIdParts[1],
601+
studyId: patientIdParts[0],
602+
};
603+
});
604+
const url = getPatientViewUrl(
605+
navCaseIds[currentIndex].studyId,
606+
navCaseIds[currentIndex].patientId,
607+
navCaseIds
608+
);
609+
610+
// Because of url length limits, we can only maintain the list in the url hash for small sets of ids.
611+
// TODO: adapt updateURL to allow for hash mutation so that we don't have manipulate window.location.hash directly
612+
this.props.onCohortIdsUpdate(newCohortIdList);
613+
if (url.length <= MAX_URL_LENGTH) {
614+
getBrowserWindow().location.hash = url.substring(
615+
url.indexOf('#') + 1
616+
);
617+
}
618+
this.urlWrapper.updateURL({
619+
studyId: navCaseIds[currentIndex].studyId,
620+
caseId: navCaseIds[currentIndex].patientId,
621+
sampleId: undefined,
622+
});
623+
}
624+
547625
@computed
548626
public get cohortNav() {
549627
if (
@@ -554,58 +632,105 @@ export class PatientViewPageInner extends React.Component<
554632
this.pageStore.studyId + ':' + this.pageStore.patientId
555633
);
556634
return (
557-
<PaginationControls
558-
currentPage={indexInCohort + 1}
559-
showMoreButton={false}
560-
showItemsPerPageSelector={false}
561-
showFirstPage={true}
562-
showLastPage={true}
563-
textBetweenButtons={` of ${this.pageStore.patientIdsInCohort.length} patients`}
564-
firstPageDisabled={indexInCohort === 0}
565-
previousPageDisabled={indexInCohort === 0}
566-
nextPageDisabled={
567-
indexInCohort ===
568-
this.pageStore.patientIdsInCohort.length - 1
569-
}
570-
lastPageDisabled={
571-
indexInCohort ===
572-
this.pageStore.patientIdsInCohort.length - 1
573-
}
574-
onFirstPageClick={() =>
575-
this.handlePatientClick(
576-
this.pageStore.patientIdsInCohort[0]
577-
)
578-
}
579-
onPreviousPageClick={() =>
580-
this.handlePatientClick(
581-
this.pageStore.patientIdsInCohort[indexInCohort - 1]
582-
)
583-
}
584-
onNextPageClick={() =>
585-
this.handlePatientClick(
586-
this.pageStore.patientIdsInCohort[indexInCohort + 1]
587-
)
588-
}
589-
onLastPageClick={() =>
590-
this.handlePatientClick(
591-
this.pageStore.patientIdsInCohort[
592-
this.pageStore.patientIdsInCohort.length - 1
593-
]
594-
)
595-
}
596-
onChangeCurrentPage={newPage => {
597-
if (
598-
newPage > 0 &&
599-
newPage <= this.pageStore.patientIdsInCohort.length
600-
) {
635+
<div
636+
style={{
637+
display: 'flex',
638+
justifyContent: 'flex-end',
639+
alignItems: 'center',
640+
}}
641+
>
642+
<PaginationControls
643+
currentPage={indexInCohort + 1}
644+
showMoreButton={false}
645+
showItemsPerPageSelector={false}
646+
showFirstPage={true}
647+
showLastPage={true}
648+
textBetweenButtons={
649+
<>
650+
<span> of </span>
651+
<DefaultTooltip
652+
placement="bottom"
653+
overlay="Open all patients in study view"
654+
>
655+
<a
656+
onClick={this.handleReturnToStudyView}
657+
target={'_blank'}
658+
>
659+
{`${this.pageStore.patientIdsInCohort.length} patients`}
660+
</a>
661+
</DefaultTooltip>
662+
</>
663+
}
664+
firstPageDisabled={indexInCohort === 0}
665+
previousPageDisabled={indexInCohort === 0}
666+
nextPageDisabled={
667+
indexInCohort ===
668+
this.pageStore.patientIdsInCohort.length - 1
669+
}
670+
lastPageDisabled={
671+
indexInCohort ===
672+
this.pageStore.patientIdsInCohort.length - 1
673+
}
674+
onFirstPageClick={() =>
601675
this.handlePatientClick(
602-
this.pageStore.patientIdsInCohort[newPage - 1]
603-
);
676+
this.pageStore.patientIdsInCohort[0]
677+
)
604678
}
605-
}}
606-
pageNumberEditable={true}
607-
className="cohortNav"
608-
/>
679+
onPreviousPageClick={() =>
680+
this.handlePatientClick(
681+
this.pageStore.patientIdsInCohort[
682+
indexInCohort - 1
683+
]
684+
)
685+
}
686+
onNextPageClick={() =>
687+
this.handlePatientClick(
688+
this.pageStore.patientIdsInCohort[
689+
indexInCohort + 1
690+
]
691+
)
692+
}
693+
onLastPageClick={() =>
694+
this.handlePatientClick(
695+
this.pageStore.patientIdsInCohort[
696+
this.pageStore.patientIdsInCohort.length - 1
697+
]
698+
)
699+
}
700+
onChangeCurrentPage={newPage => {
701+
if (
702+
newPage > 0 &&
703+
newPage <=
704+
this.pageStore.patientIdsInCohort.length
705+
) {
706+
this.handlePatientClick(
707+
this.pageStore.patientIdsInCohort[
708+
newPage - 1
709+
]
710+
);
711+
}
712+
}}
713+
pageNumberEditable={true}
714+
className="cohortNav"
715+
/>
716+
<DefaultTooltip
717+
placement="bottom"
718+
overlay="Exclude the current patient"
719+
>
720+
<span
721+
style={{
722+
marginLeft: '5px',
723+
display: 'inline-block',
724+
cursor: 'pointer',
725+
}}
726+
onClick={() =>
727+
this.handleDeletePatient(indexInCohort)
728+
}
729+
>
730+
<i className={'fa fa-minus-circle'} />
731+
</span>
732+
</DefaultTooltip>
733+
</div>
609734
);
610735
}
611736
}

src/shared/components/lazyMobXTable/LazyMobXTable.spec.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,6 @@ function getNumVisibleRows(table: ReactWrapper<any, any>): number {
125125
return getVisibleRows(table).length;
126126
}
127127

128-
function getTextBetweenButtons(
129-
table: ReactWrapper<any, any>
130-
): string | undefined {
131-
return table
132-
.find(PaginationControls)
133-
.filterWhere(x => x.hasClass('topPagination'))
134-
.props().textBetweenButtons;
135-
}
136-
137128
function getTextBeforeButtons(
138129
table: ReactWrapper<any, any>
139130
): string | undefined {

src/shared/components/paginationControls/PaginationControls.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface IPaginationControlsProps {
1818
showAllOption?: boolean;
1919
showMoreButton?: boolean;
2020
textBeforeButtons?: string;
21-
textBetweenButtons?: string;
21+
textBetweenButtons?: string | JSX.Element;
2222
firstButtonContent?: string | JSX.Element;
2323
previousButtonContent?: string | JSX.Element;
2424
nextButtonContent?: string | JSX.Element;

0 commit comments

Comments
 (0)