From 1db3f28780cd2621bc18e7e1bbb437c4ec7a0ea3 Mon Sep 17 00:00:00 2001 From: lismana Date: Thu, 19 Sep 2019 17:50:01 -0400 Subject: [PATCH] Refactor ResultsViewPage with new URLWrapper --- .circleci/config.yml | 9 +- src/pages/home/HomePage.tsx | 6 +- src/pages/resultsView/ResultsViewPage.tsx | 240 ++++++++---------- .../ResultsViewPageHelpers.spec.ts | 6 +- .../resultsView/ResultsViewPageHelpers.ts | 27 +- src/pages/resultsView/ResultsViewPageStore.ts | 234 +++++++++++------ src/pages/resultsView/ResultsViewQuery.ts | 127 --------- .../resultsView/ResultsViewURLWrapper.ts | 70 +++++ .../resultsView/download/DownloadTab.tsx | 14 +- .../components/oncoprint/OncoprintUtils.ts | 26 +- .../oncoprint/ResultsViewOncoprint.spec.tsx | 4 +- .../oncoprint/ResultsViewOncoprint.tsx | 9 +- src/shared/lib/URLWrapper.spec.ts | 93 +++++++ src/shared/lib/URLWrapper.ts | 60 ++++- src/shared/lib/setWindowVariable.ts | 11 +- 15 files changed, 547 insertions(+), 389 deletions(-) delete mode 100644 src/pages/resultsView/ResultsViewQuery.ts create mode 100644 src/pages/resultsView/ResultsViewURLWrapper.ts create mode 100644 src/shared/lib/URLWrapper.spec.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 8574b7f6f72..92dd6e7b668 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,7 @@ run_e2e_and_save_artifacts: &run_e2e_and_save_artifacts (curl --insecure https://localhost:3000 || curl http://localhost:3000) > /dev/null && \ sleep 1s && \ cd end-to-end-test && \ + echo "CBIOPORTAL_URL=$CBIOPORTAL_URL" yarn run e2e when: always environment: @@ -198,14 +199,14 @@ jobs: ./setup_environment.sh && ./setup_environment.sh >> $BASH_ENV - run: name: Generate checksum of data that populates the test database - command: | + command: | $TEST_HOME/local/runtime-config/db_content_fingerprint.sh > /tmp/db_data_md5key - restore_cache: keys: - v4-e2e-database-files-{{ checksum "/tmp/db_data_md5key" }} - run: name: Create MySQL data directory when no cache found - command: | + command: | mkdir -p $DB_DATA_DIR - run: name: Setup docker images and containers @@ -219,7 +220,7 @@ jobs: fi - run: name: Change owner of MySQL database files (needed by cache) - command: | + command: | sudo chmod -R 777 $DB_DATA_DIR && \ sudo chown -R circleci:circleci $DB_DATA_DIR - save_cache: @@ -228,7 +229,7 @@ jobs: key: v4-e2e-database-files-{{ checksum "/tmp/db_data_md5key" }} - run: name: Run end-2-end tests with studies in local database - command: | + command: | cd $PORTAL_SOURCE_DIR && $TEST_HOME/local/runtime-config/run_container_screenshot_test.sh - run: name: "Make sure all screenshots are tracked (otherwise the test will always be successful)" diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index bc626e9f657..0c48702da03 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -14,6 +14,7 @@ import getBrowserWindow from "../../public-lib/lib/getBrowserWindow"; // tslint:disable-next-line:no-import-side-effect import "./homePage.scss"; import autobind from "autobind-decorator"; +import {ResultsViewTab} from "pages/resultsView/ResultsViewPageHelpers"; (Chart as any).plugins.register({ beforeDraw: function (chartInstance: any) { @@ -41,7 +42,10 @@ export function createQueryStore(currentQuery?:any) { query.cancer_study_list = query.cancer_study_list || query.cancer_study_id; delete query.cancer_study_id; - win.routingStore.updateRoute(query, "results", true); + const tab = (queryStore.physicalStudyIdsInSelection.length > 1 && queryStore.geneIds.length === 1) ? + ResultsViewTab.CANCER_TYPES_SUMMARY : ResultsViewTab.ONCOPRINT; + + win.routingStore.updateRoute(query, `results/${tab}`, true); }; diff --git a/src/pages/resultsView/ResultsViewPage.tsx b/src/pages/resultsView/ResultsViewPage.tsx index dd66da3b242..c8c69f7e261 100644 --- a/src/pages/resultsView/ResultsViewPage.tsx +++ b/src/pages/resultsView/ResultsViewPage.tsx @@ -38,117 +38,117 @@ import { doesQueryHaveCNSegmentData, } from './ResultsViewPageStoreUtils'; import { AppStore } from '../../AppStore'; -import { updateResultsViewQuery } from './ResultsViewQuery'; import { trackQuery } from '../../shared/lib/tracking'; -import { onMobxPromise } from '../../shared/lib/onMobxPromise'; import QueryAndDownloadTabs from 'shared/components/query/QueryAndDownloadTabs'; import { createQueryStore } from 'pages/home/HomePage'; import ExtendedRouterStore from 'shared/lib/ExtendedRouterStore'; -import { CancerStudyQueryUrlParams } from '../../shared/components/query/QueryStore'; import GeneSymbolValidationError from 'shared/components/query/GeneSymbolValidationError'; +import ResultsViewURLWrapper from 'pages/resultsView/ResultsViewURLWrapper'; +import setWindowVariable from 'shared/lib/setWindowVariable'; -function initStore(appStore: AppStore) { +function initStore(appStore: AppStore, urlWrapper: ResultsViewURLWrapper) { const resultsViewPageStore = new ResultsViewPageStore( appStore, - getBrowserWindow().globalStores.routing + getBrowserWindow().globalStores.routing, + urlWrapper ); - resultsViewPageStore.tabId = getTabId( - getBrowserWindow().globalStores.routing.location.pathname - ); - - let lastQuery: any; - let lastPathname: string; - - const queryReactionDisposer = reaction( + reaction( + () => [resultsViewPageStore.studyIds, resultsViewPageStore.oqlText], () => { - return [ - getBrowserWindow().globalStores.routing.query, - getBrowserWindow().globalStores.routing.location.pathname, - ]; - }, - (x: any) => { - const query = x[0] as CancerStudyQueryUrlParams; - const pathname = x[1]; - - // escape from this if queryies are deeply equal - // TODO: see if we can figure out why query is getting changed and - // if there's any way to do shallow equality check to avoid this expensive operation - const queryChanged = !_.isEqual(lastQuery, query); - const pathnameChanged = pathname !== lastPathname; - if (!queryChanged && !pathnameChanged) { - return; - } else { - if ( - !getBrowserWindow().globalStores.routing.location.pathname.includes( - '/results' - ) - ) { - return; - } - runInAction(() => { - // set query and pathname separately according to which changed, to avoid unnecessary - // recomputation by updating the query if only the pathname changed - if (queryChanged) { - // update query - // normalize cancer_study_list this handles legacy sessions/urls where queries with single study had different param name - const cancer_study_list = - query.cancer_study_list || query.cancer_study_id; - - const cancerStudyIds: string[] = cancer_study_list.split( - ',' - ); - - const oql = decodeURIComponent(query.gene_list); - - let samplesSpecification = parseSamplesSpecifications( - query, - cancerStudyIds - ); - - const changes = updateResultsViewQuery( - resultsViewPageStore.rvQuery, - query, - samplesSpecification, - cancerStudyIds, - oql - ); - if (changes.cohortIdsList) { - resultsViewPageStore.initDriverAnnotationSettings(); - } - - onMobxPromise(resultsViewPageStore.studyIds, () => { - try { - trackQuery( - resultsViewPageStore.studyIds.result!, - oql, - resultsViewPageStore.hugoGeneSymbols, - resultsViewPageStore.queriedVirtualStudies - .result!.length > 0 - ); - } catch {} - }); - - lastQuery = query; - } - if (pathnameChanged) { - // need to set tab like this instead of with injected via params.tab because we need to set the tab - // at the same time as we set the query parameters, otherwise we get race conditions where the tab - // we're on at the time we update the query doesnt get unmounted because we change the query, causing - // MSKTabs unmounting, THEN change the tab. - const tabId = getTabId(pathname); - if (resultsViewPageStore.tabId !== tabId) { - resultsViewPageStore.tabId = tabId; - } - lastPathname = pathname; - } - }); + if (resultsViewPageStore.studyIds.isComplete) { + trackQuery( + resultsViewPageStore.studyIds.result!, + resultsViewPageStore.oqlText, + resultsViewPageStore.hugoGeneSymbols, + resultsViewPageStore.queriedVirtualStudies.result!.length > + 0 + ); } - }, - { fireImmediately: true } + } ); - resultsViewPageStore.queryReactionDisposer = queryReactionDisposer; + // whenever study list changes, reinit driver annotation settings + // const driverAnnotationsReactionDisposer = reaction( + // ()=>urlWrapper.query.cancer_study_list, + // ()=>{ + // resultsViewPageStore.initDriverAnnotationSettings(); + // }, + // { fireImmediately:true } + // ); + + // let lastQuery:any; + // let lastPathname:string; + // + // const queryReactionDisposer = reaction( + // () => { + // return [getBrowserWindow().globalStores.routing.query, getBrowserWindow().globalStores.routing.location.pathname]; + // }, + // (x:any) => { + // + // const query = x[0] as CancerStudyQueryUrlParams; + // const pathname = x[1]; + // + // // escape from this if queryies are deeply equal + // // TODO: see if we can figure out why query is getting changed and + // // if there's any way to do shallow equality check to avoid this expensive operation + // const queryChanged = !_.isEqual(lastQuery, query); + // const pathnameChanged = (pathname !== lastPathname); + // if (!queryChanged && !pathnameChanged) { + // return; + // } else { + // + // if (!getBrowserWindow().globalStores.routing.location.pathname.includes("/results")) { + // return; + // } + // runInAction(()=>{ + // // set query and pathname separately according to which changed, to avoid unnecessary + // // recomputation by updating the query if only the pathname changed + // if (queryChanged) { + // // update query + // // normalize cancer_study_list this handles legacy sessions/urls where queries with single study had different param name + // // const cancer_study_list = query.cancer_study_list || query.cancer_study_id; + // // + // // const cancerStudyIds: string[] = cancer_study_list.split(","); + // // + // // const oql = decodeURIComponent(query.gene_list); + // + // //let samplesSpecification = parseSamplesSpecifications(query, cancerStudyIds); + // + // //const changes = updateResultsViewQuery(resultsViewPageStore.rvQuery, query, [], cancerStudyIds, oql); + // + // console.log("MUST RESTORE INIT DRIVER ANNOTATION SETTINGS"); + // + // // if (changes.cohortIdsList) { + // // resultsViewPageStore.initDriverAnnotationSettings(); + // // } + // + // // onMobxPromise(resultsViewPageStore.studyIds, ()=>{ + // // try { + // // trackQuery(resultsViewPageStore.studyIds.result!, oql, resultsViewPageStore.hugoGeneSymbols, resultsViewPageStore.queriedVirtualStudies.result!.length > 0); + // // } catch {}; + // // }); + // + // //lastQuery = query; + // } + // // if (pathnameChanged) { + // // // need to set tab like this instead of with injected via params.tab because we need to set the tab + // // // at the same time as we set the query parameters, otherwise we get race conditions where the tab + // // // we're on at the time we update the query doesnt get unmounted because we change the query, causing + // // // MSKTabs unmounting, THEN change the tab. + // // const tabId = getTabId(pathname); + // // if (resultsViewPageStore.tabId !== tabId) { + // // resultsViewPageStore.tabId = tabId; + // // } + // // lastPathname = pathname; + // // } + // }); + // } + // }, + // {fireImmediately: true} + // ); + // + // //resultsViewPageStore.queryReactionDisposer = queryReactionDisposer; return resultsViewPageStore; } @@ -171,14 +171,20 @@ export default class ResultsViewPage extends React.Component< > { private resultsViewPageStore: ResultsViewPageStore; + private urlWrapper: ResultsViewURLWrapper; + @observable showTabs = true; constructor(props: IResultsViewPageProps) { super(props); - this.resultsViewPageStore = initStore(props.appStore); + this.urlWrapper = new ResultsViewURLWrapper(props.routing); - getBrowserWindow().resultsViewPageStore = this.resultsViewPageStore; + setWindowVariable('urlWrapper', this.urlWrapper); + + this.resultsViewPageStore = initStore(props.appStore, this.urlWrapper); + + setWindowVariable('resultsViewPageStore', this.resultsViewPageStore); } private handleTabChange(id: string, replace?: boolean) { @@ -201,7 +207,7 @@ export default class ResultsViewPage extends React.Component< } componentWillUnmount() { - this.resultsViewPageStore.queryReactionDisposer(); + this.resultsViewPageStore.destroy(); } @computed @@ -222,7 +228,6 @@ export default class ResultsViewPage extends React.Component< divId={'oncoprintDiv'} store={store} key={store.hugoGeneSymbols.join(',')} - routing={this.props.routing} addOnBecomeVisibleListener={ addOnBecomeVisibleListener } @@ -438,8 +443,7 @@ export default class ResultsViewPage extends React.Component< 0 @@ -448,7 +452,7 @@ export default class ResultsViewPage extends React.Component< : '' } zScoreThreshold={ - store.rvQuery.zScoreThreshold + store.zScoreThreshold } caseSetId={ store.sampleLists.result!.length > 0 @@ -571,25 +575,6 @@ export default class ResultsViewPage extends React.Component< return isRoutedTo || (!isExcludedInList && !isExcluded); } - public currentTab(tabId: string | undefined): string { - // if we have no tab defined (query submission, no tab click) - // we need to evaluate which should be the default tab - // this can only be determined by know the count of physical studies in the query - // (for virtual studies we need to fetch data determine constituent physical studies) - if (tabId === undefined) { - if ( - this.resultsViewPageStore.studies.result!.length > 1 && - this.resultsViewPageStore.hugoGeneSymbols.length === 1 - ) { - return ResultsViewTab.CANCER_TYPES_SUMMARY; // cancer type study - } else { - return ResultsViewTab.ONCOPRINT; // this will resolve to first tab - } - } else { - return tabId; - } - } - @autobind private getTabHref(tabId: string) { return URL.format({ @@ -667,12 +652,11 @@ export default class ResultsViewPage extends React.Component< !this.resultsViewPageStore.isQueryInvalid && ( this.handleTabChange(id) @@ -697,7 +681,7 @@ export default class ResultsViewPage extends React.Component< ) { setTimeout(() => { this.handleTabChange( - this.currentTab(this.resultsViewPageStore.tabId), + this.resultsViewPageStore.tabId, true ); }); diff --git a/src/pages/resultsView/ResultsViewPageHelpers.spec.ts b/src/pages/resultsView/ResultsViewPageHelpers.spec.ts index d267c01e9ce..8bc0ae0957e 100644 --- a/src/pages/resultsView/ResultsViewPageHelpers.spec.ts +++ b/src/pages/resultsView/ResultsViewPageHelpers.spec.ts @@ -21,7 +21,7 @@ describe("ResultsViewPageHelpers", () => { let cancerStudyIds = ["msk_impact_2017","luad_broad","luad_tcga_pub","lung_msk_2017","luad_mskcc_2015"]; - const ret = parseSamplesSpecifications(query, cancerStudyIds); + const ret = parseSamplesSpecifications(query.case_ids, undefined, query.case_set_id, cancerStudyIds); // @ts-ignore const expectedResult = [{"studyId":"msk_impact_2017","sampleId":"P-0000036-T01-IM3"} as SamplesSpecificationElement,{"studyId":"msk_impact_2017","sampleId":"P-0010863-T01-IM5"}]; @@ -37,7 +37,7 @@ describe("ResultsViewPageHelpers", () => { // @ts-ignore let cancerStudyIds = ["msk_impact_2017","luad_broad","luad_tcga_pub","lung_msk_2017","luad_mskcc_2015"]; - const ret = parseSamplesSpecifications(query, cancerStudyIds); + const ret = parseSamplesSpecifications(query.case_ids, undefined, query.case_set_id , cancerStudyIds); // @ts-ignore const expectedResult = [{"studyId":"msk_impact_2017","sampleId":"P-0000036-T01-IM3"},{"studyId":"msk_impact_2017","sampleId":"P-0010863-T01-IM5"}]; @@ -54,4 +54,4 @@ describe("ResultsViewPageHelpers", () => { -}); \ No newline at end of file +}); diff --git a/src/pages/resultsView/ResultsViewPageHelpers.ts b/src/pages/resultsView/ResultsViewPageHelpers.ts index 5d6c188887e..495930f7c18 100644 --- a/src/pages/resultsView/ResultsViewPageHelpers.ts +++ b/src/pages/resultsView/ResultsViewPageHelpers.ts @@ -112,21 +112,26 @@ export function populateSampleSpecificationsFromVirtualStudies(samplesSpecificat } //testIt -export function parseSamplesSpecifications(query:any, cancerStudyIds:string[]): SamplesSpecificationElement[]{ +export function parseSamplesSpecifications( + case_ids:string, + sample_list_ids:string | undefined, + case_set_id:string, + cancerStudyIds:string[] +): SamplesSpecificationElement[]{ let samplesSpecifications: SamplesSpecificationElement[]; - if (query.case_ids && query.case_ids.length > 0) { - const case_ids = query.case_ids.split(/\+|\s+/); - samplesSpecifications = case_ids.map((item:string)=>{ + if (case_ids && case_ids.length > 0) { + const case_ids_parsed = case_ids.split(/\+|\s+/); + samplesSpecifications = case_ids_parsed.map((item:string)=>{ const split = item.split(":"); return { studyId:split[0], sampleId:split[1] - } + } as SamplesSpecificationElement; }); - } else if (query.sample_list_ids) { - samplesSpecifications = query.sample_list_ids.split(",").map((studyListPair:string)=>{ + } else if (sample_list_ids) { + samplesSpecifications = sample_list_ids.split(",").map((studyListPair:string)=>{ const pair = studyListPair.split(":"); return { studyId:pair[0], @@ -134,16 +139,16 @@ export function parseSamplesSpecifications(query:any, cancerStudyIds:string[]): sampleId: undefined } }); - } else if (query.case_set_id !== "all") { + } else if (case_set_id !== "all") { // by definition if there is a case_set_id, there is only one study samplesSpecifications = cancerStudyIds.map((studyId:string)=>{ return { studyId: studyId, - sampleListId: query.case_set_id, + sampleListId: case_set_id, sampleId: undefined }; }); - } else if (query.case_set_id === "all") { // case_set_id IS equal to all + } else if (case_set_id === "all") { // case_set_id IS equal to all samplesSpecifications = cancerStudyIds.map((studyId:string)=>{ return { studyId, @@ -157,4 +162,4 @@ export function parseSamplesSpecifications(query:any, cancerStudyIds:string[]): return samplesSpecifications; -} \ No newline at end of file +} diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index 91f89606dea..492a61ad5e2 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -30,7 +30,7 @@ import { ReferenceGenomeGene, } from 'shared/api/generated/CBioPortalAPI'; import client from 'shared/api/cbioportalClientInstance'; -import { action, computed, observable, ObservableMap } from 'mobx'; +import { action, computed, observable, ObservableMap, reaction } from 'mobx'; import { remoteData } from 'public-lib/api/remoteData'; import { cached, labelMobxPromises, MobxPromise } from 'mobxpromise'; import OncoKbEvidenceCache from 'shared/cache/OncoKbEvidenceCache'; @@ -72,7 +72,6 @@ import MutationDataCache from '../../shared/cache/MutationDataCache'; import AccessorsForOqlFilter, { SimplifiedMutationType, } from '../../shared/lib/oql/AccessorsForOqlFilter'; -import { AugmentedData, CacheData } from '../../shared/lib/LazyMobXCache'; import { PatientSurvival } from '../../shared/model/PatientSurvival'; import { doesQueryContainMutationOQL, @@ -136,7 +135,7 @@ import { makeEnrichmentDataPromise, fetchPatients, FilteredAndAnnotatedMutationsReport, - compileMutations, + compileMutations, getMolecularProfiles, } from './ResultsViewPageStoreUtils'; import MobxPromiseCache from '../../shared/lib/MobxPromiseCache'; import { isSampleProfiledInMultiple } from '../../shared/lib/isSampleProfiled'; @@ -153,6 +152,7 @@ import { isMutation } from '../../shared/lib/CBioPortalAPIUtils'; import { VariantAnnotation } from 'public-lib/api/generated/GenomeNexusAPI'; import { ServerConfigHelpers } from '../../config/config'; import { + parseSamplesSpecifications, populateSampleSpecificationsFromVirtualStudies, ResultsViewTab, substitutePhysicalStudiesForVirtualStudies, @@ -169,7 +169,6 @@ import { makeComparisonGroupClinicalAttributes, makeProfiledInClinicalAttributes, } from '../../shared/components/oncoprint/ResultsViewOncoprintUtils'; -import { ResultsViewQuery } from './ResultsViewQuery'; import { annotateAlterationTypes } from '../../shared/lib/oql/annotateAlterationTypes'; import { ErrorMessages } from '../../shared/enums/ErrorEnums'; import { @@ -205,11 +204,15 @@ import { } from 'pages/studyView/StudyViewPageStore'; import { IVirtualStudyProps } from 'pages/studyView/virtualStudy/VirtualStudy'; import { decideMolecularProfileSortingOrder } from './download/DownloadUtils'; +import ResultsViewURLWrapper from "pages/resultsView/ResultsViewURLWrapper"; type Optional = | { isApplicable: true; value: T } | { isApplicable: false; value?: undefined }; +const DEFAULT_RPPA_THRESHOLD = 2; +const DEFAULT_Z_SCORE_THRESHOLD = 2; + export const AlterationTypeConstants = { MUTATION_EXTENDED: 'MUTATION_EXTENDED', COPY_NUMBER_ALTERATION: 'COPY_NUMBER_ALTERATION', @@ -432,9 +435,15 @@ export type ModifyQueryParams = { /* chronological setup concerns, rather than on encapsulation and public API */ /* tslint:disable: member-ordering */ export class ResultsViewPageStore { - constructor(private appStore: AppStore, private routing: any) { + constructor( + private appStore: AppStore, + private routing: any, + urlWrapper: ResultsViewURLWrapper + ) { labelMobxPromises(this); + this.urlWrapper = urlWrapper; + // addErrorHandler((error: any) => { // this.ajaxErrors.push(error); // }); @@ -515,14 +524,70 @@ export class ResultsViewPageStore { }, }); - this.initDriverAnnotationSettings(); + this.driverAnnotationsReactionDisposer = reaction( + () => this.urlWrapper.query.cancer_study_list, + () => { + this.initDriverAnnotationSettings(); + }, + { fireImmediately: true } + ); + } + + destroy() { + this.driverAnnotationsReactionDisposer(); + } + + public urlWrapper: ResultsViewURLWrapper; + + public driverAnnotationsReactionDisposer: any; + + @computed get oqlText() { + return this.urlWrapper.query.gene_list; + } + + @computed get genesetIds() { + return this.urlWrapper.query.geneset_list && + this.urlWrapper.query.geneset_list.trim().length + ? this.urlWrapper.query.geneset_list.trim().split(/\s+/) + : []; + } + + @computed get treatmentList() { + return this.urlWrapper.query.treatment_list && + this.urlWrapper.query.treatment_list.trim().length + ? this.urlWrapper.query.treatment_list.trim().split(/;/) + : []; + } + + @computed + get cancerStudyIds() { + return this.urlWrapper.query.cancer_study_list.split(','); + } + + @computed + get rppaScoreThreshold() { + return this.urlWrapper.query.RPPA_SCORE_THRESHOLD + ? parseFloat(this.urlWrapper.query.RPPA_SCORE_THRESHOLD) + : DEFAULT_RPPA_THRESHOLD; + } + + @computed + get zScoreThreshold() { + return this.urlWrapper.query.Z_SCORE_THRESHOLD + ? parseFloat(this.urlWrapper.query.Z_SCORE_THRESHOLD) + : DEFAULT_Z_SCORE_THRESHOLD; } - public queryReactionDisposer: any; + @computed + get selectedMolecularProfileIds() { + return getMolecularProfiles(this.urlWrapper.query); + } - public rvQuery: ResultsViewQuery = new ResultsViewQuery(); + //@observable tabId: ResultsViewTab|undefined = undefined; - @observable tabId: ResultsViewTab | undefined = undefined; + @computed get tabId() { + return this.urlWrapper.tabId || ResultsViewTab.ONCOPRINT; + } @observable public checkingVirtualStudies = false; @@ -531,7 +596,11 @@ export class ResultsViewPageStore { @observable public urlValidationError: string | null = null; @computed get profileFilter() { - return this.rvQuery.profileFilter || 0; + if (this.urlWrapper.query.profileFilter) { + return parseInt(this.urlWrapper.query.profileFilter, 10); + } else { + return 0; + } } @observable ajaxErrors: Error[] = []; @@ -692,19 +761,34 @@ export class ResultsViewPageStore { } @computed get hugoGeneSymbols() { - if (this.rvQuery.oqlQuery.length > 0) { - return uniqueGenesInOQLQuery(this.rvQuery.oqlQuery); + if (this.urlWrapper.query.gene_list.length > 0) { + return uniqueGenesInOQLQuery(this.urlWrapper.query.gene_list); } else { return []; } } @computed get queryContainsOql() { - return doesQueryContainOQL(this.rvQuery.oqlQuery); + return doesQueryContainOQL(this.urlWrapper.query.gene_list); } @computed get queryContainsMutationOql() { - return doesQueryContainMutationOQL(this.rvQuery.oqlQuery); + return doesQueryContainMutationOQL(this.urlWrapper.query.gene_list); + } + + @computed get sampleListCategory(): SampleListCategoryType | undefined { + if ( + this.urlWrapper.query.case_set_id && + [ + SampleListCategoryType.w_mut, + SampleListCategoryType.w_cna, + SampleListCategoryType.w_mut_cna, + ].includes(this.urlWrapper.query.case_set_id as any) + ) { + return this.urlWrapper.query.case_set_id as SampleListCategoryType; + } else { + return undefined; + } } public initDriverAnnotationSettings() { @@ -768,7 +852,7 @@ export class ResultsViewPageStore { // derive default profiles based on profileFilter (refers to old data priority) if ( this.studies.result.length > 1 || - this.rvQuery.selectedMolecularProfileIds.length === 0 + this.selectedMolecularProfileIds.length === 0 ) { return Promise.resolve( getDefaultMolecularProfiles( @@ -780,7 +864,7 @@ export class ResultsViewPageStore { // if we have only one study, then consult the selectedMolecularProfileIds because // user can directly select set const idLookupMap = _.keyBy( - this.rvQuery.selectedMolecularProfileIds, + this.selectedMolecularProfileIds, (id: string) => id ); // optimization return Promise.resolve( @@ -830,9 +914,7 @@ export class ResultsViewPageStore { } } // add any groups that are referenced in URL - const clinicalTracksParam = this.routing.location.query[ - CLINICAL_TRACKS_URL_PARAM - ]; + const clinicalTracksParam = this.urlWrapper.query.clinicallist; if (clinicalTracksParam) { const groupIds = clinicalTracksParam .split(',') // split by comma @@ -1466,7 +1548,7 @@ export class ResultsViewPageStore { this._filteredAndAnnotatedMutationsReport.result!, data => filterCBioPortalWebServiceData( - this.rvQuery.oqlQuery, + this.oqlText, data, new AccessorsForOqlFilter( this.selectedMolecularProfiles.result! @@ -1490,7 +1572,7 @@ export class ResultsViewPageStore { this._filteredAndAnnotatedMolecularDataReport.result!, data => filterCBioPortalWebServiceData( - this.rvQuery.oqlQuery, + this.oqlText, data, new AccessorsForOqlFilter( this.selectedMolecularProfiles.result! @@ -1510,7 +1592,7 @@ export class ResultsViewPageStore { this.defaultOQLQuery, ], invoke: () => { - if (this.rvQuery.oqlQuery.trim() != '') { + if (this.oqlText.trim() != '') { let data: ( | AnnotatedMutation | AnnotatedNumericGeneMolecularData)[] = []; @@ -1520,7 +1602,7 @@ export class ResultsViewPageStore { ); return Promise.resolve( filterCBioPortalWebServiceData( - this.rvQuery.oqlQuery, + this.oqlText, data, new AccessorsForOqlFilter( this.selectedMolecularProfiles.result! @@ -1601,13 +1683,13 @@ export class ResultsViewPageStore { const samples = this.samples.result!; const patients = this.patients.result!; - if (this.rvQuery.oqlQuery.trim() === '') { + if (this.oqlText.trim() === '') { return Promise.resolve([]); } else { const filteredAlterationsByOQLLine: UnflattenedOQLLineFilterOutput< AnnotatedExtendedAlteration >[] = filterCBioPortalWebServiceDataByUnflattenedOQLLine( - this.rvQuery.oqlQuery, + this.oqlText, data, accessorsInstance, defaultOQLQuery @@ -1643,7 +1725,7 @@ export class ResultsViewPageStore { return getSampleAlteredMap( this.oqlFilteredCaseAggregatedDataByUnflattenedOQLLine.result!, this.samples.result, - this.rvQuery.oqlQuery, + this.oqlText, this.coverageInformation.result, this.selectedMolecularProfiles.result!.map( profile => profile.molecularProfileId @@ -1665,13 +1747,13 @@ export class ResultsViewPageStore { this.patients, ], invoke: () => { - if (this.rvQuery.oqlQuery.trim() === '') { + if (this.oqlText.trim() === '') { return Promise.resolve([]); } else { const filteredAlterationsByOQLLine: OQLLineFilterOutput< AnnotatedExtendedAlteration >[] = filterCBioPortalWebServiceDataByOQLLine( - this.rvQuery.oqlQuery, + this.oqlText, [ ...this.filteredAndAnnotatedMutations.result!, ...this.filteredAndAnnotatedMolecularData.result!, @@ -2025,8 +2107,8 @@ export class ResultsViewPageStore { return Promise.resolve( buildDefaultOQLProfile( profileTypes, - this.rvQuery.zScoreThreshold, - this.rvQuery.rppaScoreThreshold + this.zScoreThreshold, + this.rppaScoreThreshold ) ); }, @@ -2428,6 +2510,15 @@ export class ResultsViewPageStore { }, }); + @computed get samplesSpecificationParams() { + return parseSamplesSpecifications( + this.urlWrapper.query.case_ids, + this.urlWrapper.query.sample_list_ids, + this.urlWrapper.query.case_set_id, + this.cancerStudyIds + ); + } + readonly samplesSpecification = remoteData({ await: () => [this.queriedVirtualStudies], invoke: async () => { @@ -2435,14 +2526,14 @@ export class ResultsViewPageStore { // if YES, we need to derive the sample lists by: // 1. looking up all sample lists in selected studies // 2. using those with matching category - if (!this.rvQuery.sampleListCategory) { + if (!this.sampleListCategory) { if (this.queriedVirtualStudies.result!.length > 0) { return populateSampleSpecificationsFromVirtualStudies( - this.rvQuery.samplesSpecification, + this.samplesSpecificationParams, this.queriedVirtualStudies.result! ); } else { - return this.rvQuery.samplesSpecification; + return this.samplesSpecificationParams; } } else { // would be nice to have an endpoint that would return multiple sample lists @@ -2451,11 +2542,11 @@ export class ResultsViewPageStore { // get sample specifications from physical studies if we are querying virtual study if (this.queriedVirtualStudies.result!.length > 0) { samplesSpecifications = populateSampleSpecificationsFromVirtualStudies( - this.rvQuery.samplesSpecification, + this.samplesSpecificationParams, this.queriedVirtualStudies.result! ); } else { - samplesSpecifications = this.rvQuery.samplesSpecification; + samplesSpecifications = this.samplesSpecificationParams; } // get unique study ids to reduce the API requests const uniqueStudyIds = _.chain(samplesSpecifications) @@ -2473,7 +2564,7 @@ export class ResultsViewPageStore { const category = SampleListCategoryTypeToFullId[ - this.rvQuery.sampleListCategory! + this.sampleListCategory! ]; const specs = allSampleLists.reduce( ( @@ -2534,7 +2625,7 @@ export class ResultsViewPageStore { await: () => [this.allStudies], invoke: async () => { const allCancerStudies = this.allStudies.result; - const cancerStudyIds = this.rvQuery.cohortIdsList; + const cancerStudyIds = this.cancerStudyIds; const missingFromCancerStudies = _.differenceWith( cancerStudyIds, @@ -2571,11 +2662,11 @@ export class ResultsViewPageStore { if (this.queriedVirtualStudies.result!.length > 0) { // we want to replace virtual studies with their underlying physical studies physicalStudies = substitutePhysicalStudiesForVirtualStudies( - this.rvQuery.cohortIdsList, + this.cancerStudyIds, this.queriedVirtualStudies.result! ); } else { - physicalStudies = this.rvQuery.cohortIdsList.slice(); + physicalStudies = this.cancerStudyIds.slice(); } return Promise.resolve(physicalStudies); }, @@ -2591,7 +2682,7 @@ export class ResultsViewPageStore { await: () => [this.allStudies, this.queriedVirtualStudies], invoke: () => { const allCancerStudies = this.allStudies.result; - const cancerStudyIds = this.rvQuery.cohortIdsList; + const cancerStudyIds = this.cancerStudyIds; const missingFromCancerStudies = _.differenceWith( cancerStudyIds, @@ -2748,7 +2839,7 @@ export class ResultsViewPageStore { mutationGroups, mutations => filterCBioPortalWebServiceData( - this.rvQuery.oqlQuery, + this.oqlText, mutations, new AccessorsForOqlFilter( this.selectedMolecularProfiles.result! @@ -3265,10 +3356,10 @@ export class ResultsViewPageStore { readonly queriedStudies = remoteData({ await: () => [this.studyIdToStudy, this.queriedVirtualStudies], invoke: async () => { - if (!_.isEmpty(this.rvQuery.cohortIdsList)) { + if (!_.isEmpty(this.cancerStudyIds)) { return fetchQueriedStudies( this.studyIdToStudy.result, - this.rvQuery.cohortIdsList, + this.cancerStudyIds, this.queriedVirtualStudies.result ? this.queriedVirtualStudies.result : [] @@ -3321,28 +3412,6 @@ export class ResultsViewPageStore { [] ); - // If we have same profile accros multiple studies, they should have the same name, so we can group them by name to get all related molecular profiles in multiple studies. - readonly nonSelectedMolecularProfilesGroupByName = remoteData<{ - [profileName: string]: MolecularProfile[]; - }>( - { - await: () => [this.nonSelectedMolecularProfiles], - invoke: () => { - const sortedProfiles = _.sortBy( - this.nonSelectedMolecularProfiles.result, - profile => - decideMolecularProfileSortingOrder( - profile.molecularAlterationType - ) - ); - return Promise.resolve( - _.groupBy(sortedProfiles, profile => profile.name) - ); - }, - }, - {} - ); - readonly molecularProfileIdToMolecularProfile = remoteData<{ [molecularProfileId: string]: MolecularProfile; }>( @@ -3368,6 +3437,29 @@ export class ResultsViewPageStore { {} ); + + // If we have same profile accros multiple studies, they should have the same name, so we can group them by name to get all related molecular profiles in multiple studies. + readonly nonSelectedMolecularProfilesGroupByName = remoteData<{ + [profileName: string]: MolecularProfile[]; + }>( + { + await: () => [this.nonSelectedMolecularProfiles], + invoke: () => { + const sortedProfiles = _.sortBy( + this.nonSelectedMolecularProfiles.result, + profile => + decideMolecularProfileSortingOrder( + profile.molecularAlterationType + ) + ); + return Promise.resolve( + _.groupBy(sortedProfiles, profile => profile.name) + ); + }, + }, + {} + ); + readonly studyToMolecularProfileDiscrete = remoteData<{ [studyId: string]: MolecularProfile; }>( @@ -3648,9 +3740,9 @@ export class ResultsViewPageStore { readonly genesets = remoteData({ invoke: () => { - if (this.rvQuery.genesetIds && this.rvQuery.genesetIds.length > 0) { + if (this.genesetIds && this.genesetIds.length > 0) { return internalClient.fetchGenesetsUsingPOST({ - genesetIds: this.rvQuery.genesetIds.slice(), + genesetIds: this.genesetIds.slice(), }); } else { return Promise.resolve([]); @@ -3710,7 +3802,7 @@ export class ResultsViewPageStore { readonly selectedTreatments = remoteData({ await: () => [this.treatmentsInStudies], invoke: () => { - const treatmentIdFromUrl = this.rvQuery.treatmentIds; + const treatmentIdFromUrl = this.treatmentList; return Promise.resolve( _.filter(this.treatmentsInStudies.result!, (d: Treatment) => treatmentIdFromUrl.includes(d.treatmentId) @@ -3729,9 +3821,9 @@ export class ResultsViewPageStore { readonly genesetLinkMap = remoteData<{ [genesetId: string]: string }>({ invoke: async () => { - if (this.rvQuery.genesetIds && this.rvQuery.genesetIds.length) { + if (this.genesetIds && this.genesetIds.length) { const genesets = await internalClient.fetchGenesetsUsingPOST({ - genesetIds: this.rvQuery.genesetIds.slice(), + genesetIds: this.genesetIds.slice(), }); const linkMap: { [genesetId: string]: string } = {}; genesets.forEach(({ genesetId, refLink }) => { @@ -3746,7 +3838,7 @@ export class ResultsViewPageStore { readonly treatmentLinkMap = remoteData<{ [treatmentId: string]: string }>({ invoke: async () => { - if (this.rvQuery.treatmentIds && this.rvQuery.treatmentIds.length) { + if (this.treatmentList && this.treatmentList.length) { const treatments = await internalClient.fetchTreatmentsUsingPOST( { treatmentFilter: { diff --git a/src/pages/resultsView/ResultsViewQuery.ts b/src/pages/resultsView/ResultsViewQuery.ts deleted file mode 100644 index b4282b7165b..00000000000 --- a/src/pages/resultsView/ResultsViewQuery.ts +++ /dev/null @@ -1,127 +0,0 @@ -import {SamplesSpecificationElement, SampleListCategoryType} from "./ResultsViewPageStore"; -import {computed, observable} from "mobx"; -import {CancerStudyQueryUrlParams} from "../../shared/components/query/QueryStore"; -import {getMolecularProfiles} from "./ResultsViewPageStoreUtils"; -import hashString from "../../shared/lib/hashString"; -import _ from "lodash"; - -export class ResultsViewQuery { - // TODO: make sure i am initializing these the exact same way as they were in resultsviewpagestore to avoid any weird bugs w assuming undefined at start - @observable.ref public samplesSpecification:SamplesSpecificationElement[] = []; - @observable public selectedStudyIds:string[] = []; - @observable public sampleListCategory:SampleListCategoryType | undefined; - @observable public profileFilter:number = 0; // maps to the old data_priority parameter - @observable public selectedMolecularProfileIds:string[] = []; - @observable public _rppaScoreThreshold:number|undefined; - @observable public _zScoreThreshold:number|undefined; - @observable public genesetIds:string[] = []; - @observable public treatmentIds:string[] = []; - @observable public cohortIdsList:string[] = [];//queried id(any combination of physical and virtual studies) - @observable public oqlQuery:string = ""; - - @computed get hash():number { - const hashKeys:(keyof ResultsViewQuery)[] = [ - "samplesSpecification", "selectedStudyIds", "sampleListCategory", "profileFilter", "selectedMolecularProfileIds", - "rppaScoreThreshold", "zScoreThreshold", "genesetIds", "cohortIdsList", "oqlQuery" - ]; - const stringified = hashKeys.reduce((acc, nextKey)=>`${acc},${nextKey}:${this[nextKey]}`, ""); - return hashString(stringified); - } - @computed public get zScoreThreshold() { - if (this._zScoreThreshold === undefined) { - return 2; - } else { - return this._zScoreThreshold; - } - } - public set zScoreThreshold(val:number) { - if (!Number.isNaN(val)) { - this._zScoreThreshold = val; - } - } - @computed public get rppaScoreThreshold() { - return this._rppaScoreThreshold === undefined ? 2 : this._rppaScoreThreshold; - } - public set rppaScoreThreshold(val:number) { - if (!Number.isNaN(val)) { - this._rppaScoreThreshold = val; - } - } -} -export function updateResultsViewQuery( - rvQuery:ResultsViewQuery, - urlQuery:CancerStudyQueryUrlParams, - samplesSpecification:SamplesSpecificationElement[], - cancerStudyIds:string[], - oql:string -) { - const trackedChanges:{[key in keyof ResultsViewQuery]?:boolean} = {}; // not comprehensive - only maintained as needed - - if (!rvQuery.samplesSpecification || !_.isEqual(rvQuery.samplesSpecification.slice(), samplesSpecification)) { - rvQuery.samplesSpecification = samplesSpecification; - } - - // set the study Ids - if (rvQuery.selectedStudyIds !== cancerStudyIds) { - rvQuery.selectedStudyIds = cancerStudyIds; - } - - // sometimes the submitted case_set_id is not actually a case_set_id but - // a category of case set ids (e.g. selected studies > 1 and case category selected) - // in that case, note that on the query - if (urlQuery.case_set_id && [SampleListCategoryType.w_mut,SampleListCategoryType.w_cna,SampleListCategoryType.w_mut_cna].includes(urlQuery.case_set_id as any)) { - if (rvQuery.sampleListCategory !== urlQuery.case_set_id) { - rvQuery.sampleListCategory = urlQuery.case_set_id as SampleListCategoryType; - } - } else { - rvQuery.sampleListCategory = undefined; - } - - if (urlQuery.data_priority !== undefined && parseInt(urlQuery.data_priority,10) !== rvQuery.profileFilter) { - rvQuery.profileFilter = parseInt(urlQuery.data_priority,10); - } - - // note that this could be zero length if we have multiple studies - // in that case we derive default selected profiles - const profiles = getMolecularProfiles(urlQuery); - if (!rvQuery.selectedMolecularProfileIds || !_.isEqual(rvQuery.selectedMolecularProfileIds.slice(), profiles)) { - rvQuery.selectedMolecularProfileIds = profiles; - } - - if (!_.isEqual(urlQuery.RPPA_SCORE_THRESHOLD, rvQuery.rppaScoreThreshold)) { - rvQuery.rppaScoreThreshold = parseFloat(urlQuery.RPPA_SCORE_THRESHOLD); - } - - if (!_.isEqual(urlQuery.Z_SCORE_THRESHOLD, rvQuery.zScoreThreshold)) { - rvQuery.zScoreThreshold = parseFloat(urlQuery.Z_SCORE_THRESHOLD); - } - - if (urlQuery.geneset_list) { - // we have to trim because for some reason we get a single space from submission - const parsedGeneSetList = urlQuery.geneset_list.trim().length ? (urlQuery.geneset_list.trim().split(/\s+/)) : []; - if (!_.isEqual(parsedGeneSetList, rvQuery.genesetIds)) { - rvQuery.genesetIds = parsedGeneSetList; - } - } - - if (urlQuery.treatment_list) { - // we have to trim because for some reason we get a single space from submission - const parsedTreatmentList = urlQuery.treatment_list.trim().length ? (urlQuery.treatment_list.trim().split(/;/)) : []; - if (!_.isEqual(parsedTreatmentList, rvQuery.treatmentIds)) { - rvQuery.treatmentIds = parsedTreatmentList; - } - } - - // cohortIdsList will contain virtual study ids (physicalstudies will contain the phsyical studies which comprise the virtual studies) - // although resultsViewStore does - if (!rvQuery.cohortIdsList || !_.isEqual(_.sortBy(rvQuery.cohortIdsList), _.sortBy(cancerStudyIds))) { - rvQuery.cohortIdsList = cancerStudyIds; - trackedChanges.cohortIdsList = true; - } - - if (rvQuery.oqlQuery !== oql) { - rvQuery.oqlQuery = oql; - } - - return trackedChanges; -} \ No newline at end of file diff --git a/src/pages/resultsView/ResultsViewURLWrapper.ts b/src/pages/resultsView/ResultsViewURLWrapper.ts new file mode 100644 index 00000000000..12ef0233368 --- /dev/null +++ b/src/pages/resultsView/ResultsViewURLWrapper.ts @@ -0,0 +1,70 @@ +import URLWrapper from "../../shared/lib/URLWrapper"; +import ExtendedRouterStore from "../../shared/lib/ExtendedRouterStore"; +import {computed} from "mobx"; +import autobind from "autobind-decorator"; +import {ResultsViewTab} from "pages/resultsView/ResultsViewPageHelpers"; + +export type ResultsViewURLQuery = { + clinicallist:string; + gene_list:string; + cancer_study_list:string; + case_ids:string; + sample_list_ids:string; + case_set_id:string; + profileFilter:string; + RPPA_SCORE_THRESHOLD:string; + Z_SCORE_THRESHOLD:string; + geneset_list:string; + treatment_list: string; + + genetic_profile_ids_PROFILE_MUTATION_EXTENDED:string; + genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION:string; + genetic_profile_ids_PROFILE_MRNA_EXPRESSION:string; + genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION:string; + genetic_profile_ids_PROFILE_GENESET_SCORE:string; + genetic_profile_ids_GENERIC_ASSAY:string; + genetic_profile_ids:string; + +}; + +export default class ResultsViewURLWrapper extends URLWrapper { + constructor(routing:ExtendedRouterStore) { + super(routing, [ + + // NON session props here + { name:"clinicallist", isSessionProp:false }, + + + // session props here + { name: "gene_list", isSessionProp:true }, + { name: "cancer_study_list", isSessionProp:true, aliases:["cancer_study_id"] }, + { name: "case_ids", isSessionProp:true }, + { name: "sample_list_ids", isSessionProp:true }, + { name: "case_set_id", isSessionProp:true }, + { name: "profileFilter", isSessionProp:true }, + { name: "RPPA_SCORE_THRESHOLD", isSessionProp:true }, + { name: "Z_SCORE_THRESHOLD", isSessionProp:true }, + { name: "geneset_list", isSessionProp:true }, + { name: "treatment_list", isSessionProp:true }, + { name: "genetic_profile_ids_PROFILE_MUTATION_EXTENDED", isSessionProp:true }, + { name: "genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION", isSessionProp:true }, + { name: "genetic_profile_ids_PROFILE_MRNA_EXPRESSION", isSessionProp:true }, + { name: "genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION", isSessionProp:true }, + { name: "genetic_profile_ids_PROFILE_GENESET_SCORE", isSessionProp:true }, + { name: "genetic_profile_ids_GENERIC_ASSAY", isSessionProp:true }, + { name: "genetic_profile_ids", isSessionProp:true }, + + ]); + } + + pathContext = "/results"; + + @computed public get tabId() { + return this.pathName.split("/").pop(); + } + + @autobind + public setTabId(tabId:ResultsViewTab, replace?:boolean) { + this.routing.updateRoute({}, `comparison/${tabId}`, false, replace); + } +} diff --git a/src/pages/resultsView/download/DownloadTab.tsx b/src/pages/resultsView/download/DownloadTab.tsx index a4110fcd29d..cceac513ac7 100644 --- a/src/pages/resultsView/download/DownloadTab.tsx +++ b/src/pages/resultsView/download/DownloadTab.tsx @@ -87,7 +87,7 @@ export default class DownloadTab extends React.Component this.props.store.molecularProfileIdToMolecularProfile, ], invoke: ()=>Promise.resolve(generateCaseAlterationData( - this.props.store.rvQuery.oqlQuery, + this.props.store.oqlText, this.props.store.selectedMolecularProfiles.result!, this.props.store.oqlFilteredCaseAggregatedDataByOQLLine.result!, this.props.store.oqlFilteredCaseAggregatedDataByUnflattenedOQLLine.result!, @@ -227,7 +227,7 @@ export default class DownloadTab extends React.Component this.props.store.oqlFilteredCaseAggregatedDataByUnflattenedOQLLine.result!.forEach((data, index) => { // mergedTrackOqlList is undefined means the data is for single track / oql if (data.mergedTrackOqlList === undefined) { - labels.push(getSingleGeneResultKey(index, this.props.store.rvQuery.oqlQuery, data.oql as OQLLineFilterOutput)); + labels.push(getSingleGeneResultKey(index, this.props.store.oqlText, data.oql as OQLLineFilterOutput)); } // or data is for merged track (group: list of oqls) else { @@ -246,7 +246,7 @@ export default class DownloadTab extends React.Component // mergedTrackOqlList is undefined means the data is for single track / oql if (data.mergedTrackOqlList === undefined) { const singleTrackOql = data.oql as OQLLineFilterOutput; - const label = getSingleGeneResultKey(index, this.props.store.rvQuery.oqlQuery, data.oql as OQLLineFilterOutput); + const label = getSingleGeneResultKey(index, this.props.store.oqlText, data.oql as OQLLineFilterOutput); // put types for single track into the map, key is track label if (singleTrackOql.parsed_oql_line.alterations) { trackAlterationTypesMap[label] = _.uniq(_.map(singleTrackOql.parsed_oql_line.alterations, (alteration) => alteration.alteration_type.toUpperCase())); @@ -308,7 +308,7 @@ export default class DownloadTab extends React.Component }); public render() { - const status = getMobxPromiseGroupStatus(this.geneAlterationData, this.caseAlterationData, this.oqls, this.trackLabels, this.trackAlterationTypesMap, this.geneAlterationMap, this.cnaData, this.mutationData, + const status = getMobxPromiseGroupStatus(this.geneAlterationData, this.caseAlterationData, this.oqls, this.trackLabels, this.trackAlterationTypesMap, this.geneAlterationMap, this.cnaData, this.mutationData, this.mrnaData, this.proteinData, this.unalteredCaseAlterationData, this.alteredCaseAlterationData, this.props.store.virtualStudyParams, this.sampleMatrixText, this.props.store.nonSelectedMolecularProfilesGroupByName, this.props.store.studies, this.props.store.selectedMolecularProfiles); @@ -444,8 +444,8 @@ export default class DownloadTab extends React.Component }); const filteredProfileOptions = allProfileOptions.slice(0, Math.min(DOWNLOAD_TABLE_DEFAULT_ROW_LIMIT - this.props.store.selectedMolecularProfiles.result!.length, allProfileOptions.length)); - - return _.map(this.showAllRows ? allProfileOptions : filteredProfileOptions, (option) => + + return _.map(this.showAllRows ? allProfileOptions : filteredProfileOptions, (option) => ( @@ -695,4 +695,4 @@ export default class DownloadTab extends React.Component private downloadDataText(downloadData: string[][]): string { return stringify2DArray(downloadData); } -} \ No newline at end of file +} diff --git a/src/shared/components/oncoprint/OncoprintUtils.ts b/src/shared/components/oncoprint/OncoprintUtils.ts index 7f8043049d2..3882d612375 100644 --- a/src/shared/components/oncoprint/OncoprintUtils.ts +++ b/src/shared/components/oncoprint/OncoprintUtils.ts @@ -202,33 +202,33 @@ export function getTreatmentTrackRuleSetParams(trackSpec: IHeatmapTrackSpec):Rul // - The most extreme value in the legend is should be the largest value in the current track group. It is passed in // along side other track specs (if possible) // - When the most extreme value does not reach the pivotThreshold the pivotThreshold is used a most extreme value - + legend_label = `${trackSpec.molecularProfileName}`; const dataPoints = trackSpec.data; const pivotThreshold = trackSpec.pivotThreshold; const sortOrder = trackSpec.sortOrder; - + const colorBetterDark = [0,114,178,1] as [number, number, number, number]; const colorBetterLight = [204,236,255,1] as [number, number, number, number]; const colorWorseDark = [213,94,0,1] as [number, number, number, number]; const colorWorseLight = [255,226,204,1] as [number, number, number, number]; const categoryColorOptions = [ 'rgba(240,228,66,1)', 'rgba(0,158,115,1)', 'rgba(204,121,167,1)', 'rgba(0,0,0,1)' ]; - + let maxValue = trackSpec.maxProfileValue!; let minValue = trackSpec.minProfileValue!; if (pivotThreshold !== undefined) { maxValue = Math.max(maxValue, pivotThreshold); minValue = Math.min(minValue, pivotThreshold); } - + const pivotOutsideValueRange = pivotThreshold && (maxValue === pivotThreshold || minValue === pivotThreshold); - + // when all observed values are negative or positive // assume that 0 should be used in the legend const rightBoundaryValue = Math.max(0, maxValue); const leftBoundaryValue = Math.min(0, minValue); value_range = [leftBoundaryValue, rightBoundaryValue]; // larger concentrations are `better` (ASC) - + // only include the pivotValue in the legend when covered by the current value_range if (pivotThreshold === undefined || pivotOutsideValueRange) { colors = [colorBetterDark, colorBetterLight]; @@ -236,21 +236,21 @@ export function getTreatmentTrackRuleSetParams(trackSpec: IHeatmapTrackSpec):Rul } else { colors = [colorBetterDark, colorBetterLight, colorWorseLight, colorWorseDark]; if (pivotThreshold <= leftBoundaryValue) { - // when data points do not bracket the pivotThreshold, make an artificial left boundary + // when data points do not bracket the pivotThreshold, make an artificial left boundary value_stop_points = [pivotThreshold-(rightBoundaryValue-pivotThreshold), pivotThreshold, pivotThreshold, rightBoundaryValue]; } else if (pivotThreshold >= rightBoundaryValue) { - // when data points do not bracket the pivotThreshold, make an artificial right boundary + // when data points do not bracket the pivotThreshold, make an artificial right boundary value_stop_points = [leftBoundaryValue, pivotThreshold, pivotThreshold, pivotThreshold+(pivotThreshold-leftBoundaryValue)]; } else { value_stop_points = [leftBoundaryValue, pivotThreshold, pivotThreshold, rightBoundaryValue]; } } - + if (sortOrder === "DESC") { // smaller concentrations are `better` (DESC) value_range = _.reverse(value_range); value_stop_points = _.reverse(value_stop_points); } - + let counter = 0; const categories = _(dataPoints as ITreatmentHeatmapTrackDatum[]).filter((d:ITreatmentHeatmapTrackDatum) => !!d.category).map((d)=>d.category).uniq().value(); categories.forEach( (d:string) => { @@ -745,7 +745,7 @@ export function makeTreatmentProfileHeatmapTracksMobxPromise(oncoprint:ResultsVi const treatmentProfiles = _.filter(molecularProfileIdToHeatmapTracks.values(), d => d.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY); const neededTreatments = _.flatten(treatmentProfiles.map(v=>v.entities.keys())); await oncoprint.props.store.treatmentCache.getPromise(neededTreatments.map(g=>({treatmentId:g})), true); - + const cacheQueries = _.flatten(treatmentProfiles.map(entry=>( entry.entities.keys().map(g=>({ molecularProfileId: entry.molecularProfileId, @@ -906,7 +906,7 @@ export function makeGenesetHeatmapTracksMobxPromise( return []; } const molecularProfileId = molecularProfile.value.molecularProfileId; - const genesetIds = oncoprint.props.store.rvQuery.genesetIds; + const genesetIds = oncoprint.props.store.genesetIds; const cacheQueries = genesetIds.map((genesetId) => ({molecularProfileId, genesetId})); await dataCache.getPromise(cacheQueries, true); @@ -974,4 +974,4 @@ export function extractTreatmentSelections(text:string, selectedTreatments:strin export function splitHeatmapTextField(text:string):string[] { text = text.replace(/[,\s\n]+/g," ").trim(); return _.uniq(text.split(/[,\s\n]+/)); -} \ No newline at end of file +} diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx index 1552a19e904..084e561a5dc 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx @@ -75,7 +75,7 @@ describe('Oncoprint sortBy URL parameter', () => { storeMock.givenSampleOrder.isComplete = params.caselistEnabled; } const oncoprintView = new ResultsViewOncoprint( - {divId: "", store: storeMock, routing: ""} + {divId: "", store: storeMock} ); if (params.columnMode !== undefined) { oncoprintView.columnMode = params.columnMode; @@ -83,4 +83,4 @@ describe('Oncoprint sortBy URL parameter', () => { return oncoprintView; }; -}); \ No newline at end of file +}); diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx index 5478c4f9bf6..944d4e36e6f 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx @@ -44,7 +44,6 @@ import { Treatment } from "shared/api/generated/CBioPortalAPIInternal"; interface IResultsViewOncoprintProps { divId: string; store:ResultsViewPageStore; - routing:any; addOnBecomeVisibleListener?:(callback:()=>void)=>void; } @@ -59,7 +58,6 @@ export type SortMode = ( {type:"heatmap", clusteredHeatmapProfile:string} ); - export interface IGenesetExpansionRecord { entrezGeneId: number; hugoGeneSymbol: string; @@ -143,6 +141,7 @@ export default class ResultsViewOncoprint extends React.Component diff --git a/src/shared/lib/URLWrapper.spec.ts b/src/shared/lib/URLWrapper.spec.ts new file mode 100644 index 00000000000..1ff4310d189 --- /dev/null +++ b/src/shared/lib/URLWrapper.spec.ts @@ -0,0 +1,93 @@ +import { assert } from "chai"; +import ResultsViewURLWrapper from "pages/resultsView/ResultsViewURLWrapper"; +import { autorun, observable, reaction } from "mobx"; +import ExtendedRouterStore from "shared/lib/ExtendedRouterStore"; +import sinon from "sinon"; + +describe("URLWrapper", () => { + it("resolves properties aliases correctly", () => { + const fakeRouter = observable({ + query: { cancer_study_id: "some_study_id", non_property: "foo" }, + }) as any; + + const wrapper = new ResultsViewURLWrapper(fakeRouter); + + assert.equal(wrapper.query.cancer_study_list, "some_study_id", "alias resolves to correct param"); + assert.notProperty(wrapper.query, "cancer_study_id"); + }); + + it("resolves properties correctly", () => { + const fakeRouter = observable({ + query: { case_ids: "bar", non_property: "foo" }, + }) as any; + + const wrapper = new ResultsViewURLWrapper(fakeRouter); + + assert.notProperty(wrapper.query, "non_property"); + assert.equal(wrapper.query.case_ids, "bar"); + }); + + it("reacts to underling routing store according to rules", () => { + const fakeRouter = observable({ + query: { case_ids: "bar", non_property: "foo" }, + location: { pathname: "/results" }, + }) as any; + + const wrapper = new ResultsViewURLWrapper(fakeRouter); + + const stub = sinon.stub(); + + const disposer = reaction(() => wrapper.query.case_ids, stub); + + assert.equal(stub.args.length, 0, "stub hasn't been called"); + + fakeRouter.query.case_ids = "bar2"; + + assert.equal(stub.args.length, 1, "stub has been called due to update to property"); + + fakeRouter.query.case_ids = "bar2"; + + assert.equal(stub.args.length, 1, "setting property to existing value does not cause reaction"); + + fakeRouter.query.cancer_study_list = "study1"; + + assert.equal( + stub.args.length, + 1, + "setting query property which is not referenced in reaction does not cause reaction" + ); + + fakeRouter.query.case_ids = "bar3"; + + assert.equal(stub.args.length, 2, "setting property to knew value DOES cause reaction"); + + fakeRouter.location.pathname = "/patient"; + fakeRouter.query.case_ids = "bar4"; + assert.equal(stub.args.length, 2, "does not react when pathname doesn't match"); + + disposer(); + }); + + // it('hash composed only of session props', ()=>{ + // + // const fakeRouter = observable({ + // query: { case_ids: "bar", non_property: "foo" }, + // location: { pathname: "/results" }, + // }) as any; + // + // const wrapper = new ResultsViewURLWrapper(fakeRouter); + // + // const beforeChange = wrapper.hash; + // + // fakeRouter.query.clinicallist = "1,2,3"; + // + // assert.equal(wrapper.hash, beforeChange, "hash doesn't change if we mutate non session prop"); + // + // fakeRouter.query.case_ids = "blah"; + // + // assert.notEqual(wrapper.hash, beforeChange, "hash changes if we mutate session prop"); + // + // + // }); + +}); diff --git a/src/shared/lib/URLWrapper.ts b/src/shared/lib/URLWrapper.ts index 692346f1741..c4a1f2cb500 100644 --- a/src/shared/lib/URLWrapper.ts +++ b/src/shared/lib/URLWrapper.ts @@ -1,13 +1,18 @@ -import {autorun, computed, extendObservable, intercept, IObservableObject, observable} from "mobx"; +import {autorun, computed, extendObservable, intercept, IObservableObject, IReactionDisposer, observable} from "mobx"; import ExtendedRouterStore from "./ExtendedRouterStore"; +import hashString from "shared/lib/hashString"; +import * as _ from "lodash"; export type Property = { name: keyof T, - isSessionProp: boolean + isSessionProp: boolean, + aliases?: string[], }; -export default class URLWrapper { - public query:Partial; +export default class URLWrapper { + public query:QueryParamsType; + public reactionDisposer: IReactionDisposer; + protected pathContext:string; constructor( protected routing:ExtendedRouterStore, @@ -15,9 +20,9 @@ export default class URLWrapper { ) { const initValues:Partial = {}; for (const property of properties) { - initValues[property.name] = undefined; + initValues[property.name] = (routing.query as QueryParamsType)[property.name]; } - this.query = observable>(initValues); + this.query = observable(initValues as QueryParamsType); intercept(this.query, change=>{ if (change.newValue === this.query[change.name as keyof QueryParamsType]) { @@ -27,10 +32,15 @@ export default class URLWrapper { return change; } }); - autorun(()=>{ + this.reactionDisposer = autorun(()=>{ const query = routing.query as QueryParamsType; + // if there is a path context and it is not + if (this.pathContext && !(new RegExp(`^/*${this.pathContext}`)).test(routing.location.pathname)) { + return; + } for (const property of properties) { - this.query[property.name] = query[property.name]; + // @ts-ignore + this.syncProperty(property, query); } }); } @@ -39,6 +49,24 @@ export default class URLWrapper { this.routing.updateRoute(query as any); } + private syncProperty(property:Property, query:QueryParamsType){ + this.trySyncProperty(property, query[property.name]); + // if it's still undefined, then check aliases + if (this.query[property.name] === undefined && property.aliases && property.aliases.length) { + for (const alias of property.aliases) { + const synced = this.trySyncProperty(property, query[alias]); + // once you've set it, don't bother with any other aliases + if (synced) break; + } + } + } + + private trySyncProperty(property:Property, value:string|undefined){ + // @ts-ignore + this.query[property.name] = typeof value === "string" ? decodeURIComponent(value) : undefined; + return value !== undefined; + } + public getSessionProps() { const ret:Partial = {}; for (const property of this.properties) { @@ -49,7 +77,21 @@ export default class URLWrapper { return ret; } + @computed get hash():number { + const stringified = _.reduce(this.properties,(acc, nextVal)=>{ + // @ts-ignore + acc = `${acc},${nextVal.name}:${this.query[nextVal.name]}`; + return acc; + }, ""); + return hashString(stringified); + } + @computed public get pathName() { return this.routing.location.pathname; } -} \ No newline at end of file + + public destroy(){ + this.reactionDisposer(); + } + +} diff --git a/src/shared/lib/setWindowVariable.ts b/src/shared/lib/setWindowVariable.ts index 22ac62af276..13434d40a1a 100644 --- a/src/shared/lib/setWindowVariable.ts +++ b/src/shared/lib/setWindowVariable.ts @@ -1,10 +1,3 @@ export default function setWindowVariable(propName:string, value:any) { - // utility function to throw error if window variable is set twice, safer than doing (window as any).asjd which may - // accidentally overwrite something - if ((typeof (window as any)[propName] === typeof undefined) || - (window as any)[propName] === value) { - (window as any)[propName] = value; - } else { - throw new Error(`Attempted to set existing window variable '${propName}'.. SHAME ON YOU`); - } -} \ No newline at end of file + if (window) (window as any)[propName] = value; +}