diff --git a/end-to-end-test/remote/screenshots/reference/patient_page_valid_after_cohort_navigation_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/patient_page_valid_after_cohort_navigation_element_chrome_1600x1000.png index 7be88f651ba..55e0b530280 100644 Binary files a/end-to-end-test/remote/screenshots/reference/patient_page_valid_after_cohort_navigation_element_chrome_1600x1000.png and b/end-to-end-test/remote/screenshots/reference/patient_page_valid_after_cohort_navigation_element_chrome_1600x1000.png differ diff --git a/src/pages/patientView/PatientViewPage.tsx b/src/pages/patientView/PatientViewPage.tsx index e16147c0edf..6eac869b2b8 100644 --- a/src/pages/patientView/PatientViewPage.tsx +++ b/src/pages/patientView/PatientViewPage.tsx @@ -3,13 +3,16 @@ import _ from 'lodash'; import { DiscreteCopyNumberData, ResourceData } from 'cbioportal-ts-api-client'; import { PaginationControls } from '../../shared/components/paginationControls/PaginationControls'; import { IColumnVisibilityDef } from 'shared/components/columnVisibilityControls/ColumnVisibilityControls'; -import { toggleColumnVisibility } from 'cbioportal-frontend-commons'; +import { + DefaultTooltip, + toggleColumnVisibility, +} from 'cbioportal-frontend-commons'; import { PatientViewPageStore, buildCohortIdsFromNavCaseIds, } from './clinicalInformation/PatientViewPageStore'; import { inject, observer } from 'mobx-react'; -import { action, computed, observable, makeObservable } from 'mobx'; +import { action, computed, observable, makeObservable, toJS } from 'mobx'; import { default as PatientViewMutationTable } from './mutation/PatientViewMutationTable'; import { MSKTab } from '../../shared/components/MSKTabs/MSKTabs'; import ValidationAlert from 'shared/components/ValidationAlert'; @@ -18,7 +21,11 @@ import PatientViewCnaDataStore from './copyNumberAlterations/PatientViewCnaDataS import './patient.scss'; -import { getWholeSlideViewerUrl } from '../../shared/api/urls'; +import { + buildCBioPortalPageUrl, + getPatientViewUrl, + getWholeSlideViewerUrl, +} from '../../shared/api/urls'; import { PageLayout } from '../../shared/components/PageLayout/PageLayout'; import Helmet from 'react-helmet'; import { getServerConfig } from '../../config/config'; @@ -55,11 +62,13 @@ import { prepareCustomTabConfigurations } from 'shared/lib/customTabs/customTabH import setWindowVariable from 'shared/lib/setWindowVariable'; import { getNavCaseIdsCache } from 'shared/lib/handleLongUrls'; import PatientViewPageHeader from 'pages/patientView/PatientViewPageHeader'; +import { MAX_URL_LENGTH } from 'pages/studyView/studyPageHeader/ActionButtons'; export interface IPatientViewPageProps { routing: any; appStore: AppStore; cohortIds?: string[]; + onCohortIdsUpdate: (ids: string[]) => void; } export interface IGenePanelModal { @@ -87,15 +96,14 @@ export default class PatientViewPage extends React.Component< IPatientViewPageProps, {} > { - cohortIds: string[] | undefined; + @observable cohortIds: string[] | undefined; constructor(props: IPatientViewPageProps) { super(props); - + makeObservable(this); const postData = getBrowserWindow().clientPostedData; const urlData = getNavCaseIdsCache(); - if (postData && postData.navCaseIds) { this.cohortIds = buildCohortIdsFromNavCaseIds(postData.navCaseIds); getBrowserWindow().clientPostedData = null; @@ -104,12 +112,18 @@ export default class PatientViewPage extends React.Component< } } + @action.bound + updateCohortIds(newCohortIds: string[]) { + this.cohortIds = newCohortIds; + } + render() { return ( ); } @@ -544,6 +558,70 @@ export class PatientViewPageInner extends React.Component< return this.patientViewPageStore; } + @action.bound + private handleReturnToStudyView() { + const patientIdentifiers = this.pageStore.patientIdsInCohort.map(p => { + const patientIdParts = p.split(':'); + return { + patientId: patientIdParts[1], + studyId: patientIdParts[0], + }; + }); + const queriedStudies: Set = new Set( + patientIdentifiers.map(p => p.studyId) + ); + + // We need to do this because of url length limits. We post the data to the new window once it is opened. + const studyPage = window.open( + buildCBioPortalPageUrl(`study`, { + id: Array.from(queriedStudies).join(','), + }), + '_blank' + ); + if (patientIdentifiers.length > 0) { + (studyPage as any).studyPageFilter = `filterJson=${JSON.stringify({ + patientIdentifiers, + })}`; + } + } + + @action.bound + private handleDeletePatient(deleteAtIndex: number) { + var newCohortIdList = toJS(this.pageStore.patientIdsInCohort); + newCohortIdList.splice(deleteAtIndex, 1); + let currentIndex = + deleteAtIndex > newCohortIdList.length - 1 + ? deleteAtIndex - 1 + : deleteAtIndex; + + let navCaseIds = newCohortIdList.map(p => { + const patientIdParts = p.split(':'); + return { + patientId: patientIdParts[1], + studyId: patientIdParts[0], + }; + }); + const url = getPatientViewUrl( + navCaseIds[currentIndex].studyId, + navCaseIds[currentIndex].patientId, + navCaseIds + ); + + // Because of url length limits, we can only maintain the list in the url hash for small sets of ids. + // TODO: adapt updateURL to allow for hash mutation so that we don't have manipulate window.location.hash directly + this.props.onCohortIdsUpdate(newCohortIdList); + if (url.length <= MAX_URL_LENGTH) { + getBrowserWindow().location.hash = url.substring( + url.indexOf('#') + 1 + ); + } + this.urlWrapper.updateURL({ + studyId: navCaseIds[currentIndex].studyId, + caseId: navCaseIds[currentIndex].patientId, + sampleId: undefined, + }); + } + @computed public get cohortNav() { if ( @@ -554,58 +632,105 @@ export class PatientViewPageInner extends React.Component< this.pageStore.studyId + ':' + this.pageStore.patientId ); return ( - - this.handlePatientClick( - this.pageStore.patientIdsInCohort[0] - ) - } - onPreviousPageClick={() => - this.handlePatientClick( - this.pageStore.patientIdsInCohort[indexInCohort - 1] - ) - } - onNextPageClick={() => - this.handlePatientClick( - this.pageStore.patientIdsInCohort[indexInCohort + 1] - ) - } - onLastPageClick={() => - this.handlePatientClick( - this.pageStore.patientIdsInCohort[ - this.pageStore.patientIdsInCohort.length - 1 - ] - ) - } - onChangeCurrentPage={newPage => { - if ( - newPage > 0 && - newPage <= this.pageStore.patientIdsInCohort.length - ) { +
+ + of + + + {`${this.pageStore.patientIdsInCohort.length} patients`} + + + + } + firstPageDisabled={indexInCohort === 0} + previousPageDisabled={indexInCohort === 0} + nextPageDisabled={ + indexInCohort === + this.pageStore.patientIdsInCohort.length - 1 + } + lastPageDisabled={ + indexInCohort === + this.pageStore.patientIdsInCohort.length - 1 + } + onFirstPageClick={() => this.handlePatientClick( - this.pageStore.patientIdsInCohort[newPage - 1] - ); + this.pageStore.patientIdsInCohort[0] + ) } - }} - pageNumberEditable={true} - className="cohortNav" - /> + onPreviousPageClick={() => + this.handlePatientClick( + this.pageStore.patientIdsInCohort[ + indexInCohort - 1 + ] + ) + } + onNextPageClick={() => + this.handlePatientClick( + this.pageStore.patientIdsInCohort[ + indexInCohort + 1 + ] + ) + } + onLastPageClick={() => + this.handlePatientClick( + this.pageStore.patientIdsInCohort[ + this.pageStore.patientIdsInCohort.length - 1 + ] + ) + } + onChangeCurrentPage={newPage => { + if ( + newPage > 0 && + newPage <= + this.pageStore.patientIdsInCohort.length + ) { + this.handlePatientClick( + this.pageStore.patientIdsInCohort[ + newPage - 1 + ] + ); + } + }} + pageNumberEditable={true} + className="cohortNav" + /> + + + this.handleDeletePatient(indexInCohort) + } + > + + + +
); } } diff --git a/src/shared/components/lazyMobXTable/LazyMobXTable.spec.tsx b/src/shared/components/lazyMobXTable/LazyMobXTable.spec.tsx index 67e783ee680..73522a4ad62 100644 --- a/src/shared/components/lazyMobXTable/LazyMobXTable.spec.tsx +++ b/src/shared/components/lazyMobXTable/LazyMobXTable.spec.tsx @@ -125,15 +125,6 @@ function getNumVisibleRows(table: ReactWrapper): number { return getVisibleRows(table).length; } -function getTextBetweenButtons( - table: ReactWrapper -): string | undefined { - return table - .find(PaginationControls) - .filterWhere(x => x.hasClass('topPagination')) - .props().textBetweenButtons; -} - function getTextBeforeButtons( table: ReactWrapper ): string | undefined { diff --git a/src/shared/components/paginationControls/PaginationControls.tsx b/src/shared/components/paginationControls/PaginationControls.tsx index f967d89a0e8..e445e7e576f 100644 --- a/src/shared/components/paginationControls/PaginationControls.tsx +++ b/src/shared/components/paginationControls/PaginationControls.tsx @@ -18,7 +18,7 @@ export interface IPaginationControlsProps { showAllOption?: boolean; showMoreButton?: boolean; textBeforeButtons?: string; - textBetweenButtons?: string; + textBetweenButtons?: string | JSX.Element; firstButtonContent?: string | JSX.Element; previousButtonContent?: string | JSX.Element; nextButtonContent?: string | JSX.Element;