diff --git a/.env.development b/.env.development index 9f93418..b66e781 100644 --- a/.env.development +++ b/.env.development @@ -1 +1,2 @@ NEXT_PUBLIC_GEN3_COMMONS_NAME=gen3 +NEXT_PUBLIC_PROTEINPAINT_API=/auth/api/custom/proteinpaint diff --git a/.env.production b/.env.production index 9f93418..b66e781 100644 --- a/.env.production +++ b/.env.production @@ -1 +1,2 @@ NEXT_PUBLIC_GEN3_COMMONS_NAME=gen3 +NEXT_PUBLIC_PROTEINPAINT_API=/auth/api/custom/proteinpaint diff --git a/config/gen3/analysisTools.json b/config/gen3/analysisTools.json index 03f00f2..526c249 100644 --- a/config/gen3/analysisTools.json +++ b/config/gen3/analysisTools.json @@ -81,6 +81,39 @@ "hideCounts" : true, "appId" : "CohortComparison", "countUnits": "Cases" + }, + { + "title": "ProteinPaint", + "type": "application", + "hasDemo": false, + "loginRequired": false, + "description": "Visualize mutations in protein-coding genes by consequence type and protein domain.", + "icon": "/icons/apps/ProteinPaint.svg", + "appId" : "ProteinPaint", + "count": 1000, + "countUnits": "Cases" + }, + { + "title": "OncoMatrix", + "type": "application", + "hasDemo": false, + "loginRequired": false, + "description": "Visualize the top most mutated cases and genes affected by high impact mutations in your cohort.", + "icon": "/icons/apps/OncoMatrix.svg", + "appId" : "OncoMatrix", + "count": 1000, + "countUnits": "Cases" + }, + { + "title": "Set Operations", + "type": "application", + "hasDemo": false, + "loginRequired": false, + "description": "Display a Venn diagram and compare/contrast your cohorts or sets of the same type.", + "icon": "/icons/apps/SetOperations.svg", + "href": "/", + "count": 1000, + "countUnits": "Cases" } ] } diff --git a/package-lock.json b/package-lock.json index 63dfd2e..c02a2ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@next/mdx": "^15.5.7", "@oncojs/survivalplot": "^0.8.3", "@react-spring/web": "^9.7.5", + "@sjcrh/proteinpaint-client": "^2.164.1", "cookies-next": "^4.3.0", "echarts": "^5.5.1", "file-saver": "^2.0.5", @@ -10561,6 +10562,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sjcrh/proteinpaint-client": { + "version": "2.164.1", + "resolved": "https://registry.npmjs.org/@sjcrh/proteinpaint-client/-/proteinpaint-client-2.164.1.tgz", + "integrity": "sha512-Wm9EkdOWZbLTrJteyJ9udTLsWUB+9AVx+vrpG+2xEBdgTnZfF+XgxADqSYspDliGs1254SBRL46e2uiM4za9VA==", + "license": "SEE LICENSE IN ./LICENSE" + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", diff --git a/package.json b/package.json index 3cf92ea..4d1775b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@next/mdx": "^15.5.7", "@oncojs/survivalplot": "^0.8.3", "@react-spring/web": "^9.7.5", + "@sjcrh/proteinpaint-client": "^2.164.1", "cookies-next": "^4.3.0", "echarts": "^5.5.1", "file-saver": "^2.0.5", diff --git a/src/features/apps/OncoMatrix.tsx b/src/features/apps/OncoMatrix.tsx new file mode 100644 index 0000000..e609ae4 --- /dev/null +++ b/src/features/apps/OncoMatrix.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { MatrixWrapper } from "../proteinpaint/MatrixWrapper"; + +const MatrixApp: React.FC = () => { + return ; +}; + +export default MatrixApp; diff --git a/src/features/apps/ProteinPaint.tsx b/src/features/apps/ProteinPaint.tsx new file mode 100644 index 0000000..b326bf0 --- /dev/null +++ b/src/features/apps/ProteinPaint.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { ProteinPaintWrapper } from "@/features/proteinpaint/ProteinPaintWrapper"; + +const ProteinPaintApp: React.FC = () => { + return ; +}; + +export default ProteinPaintApp; diff --git a/src/features/proteinpaint/MatrixWrapper.tsx b/src/features/proteinpaint/MatrixWrapper.tsx new file mode 100644 index 0000000..484f74c --- /dev/null +++ b/src/features/proteinpaint/MatrixWrapper.tsx @@ -0,0 +1,366 @@ +import React, { useRef, useCallback, useState, FC } from "react"; +import { useDeepCompareEffect } from "use-deep-compare"; +import { bindProteinPaint } from "@sjcrh/proteinpaint-client"; +import { useIsDemoApp } from "@/hooks/useIsDemoApp"; +import { + useCoreSelector, + convertFilterSetToGqlFilter as buildCohortGqlOperator, + FilterSet, + //PROTEINPAINT_API, + useFetchUserDetailsQuery, + useCoreDispatch, + // useCreateCaseSetFromValuesMutation, + // useGetGenesQuery, + Operation, + Includes, + showModal, + hideModal, + Modals, + selectCurrentModal, +} from "@gen3/core"; +import { joinFilters, selectCurrentCohortCaseFilters } from "@/core/utils"; +import { DEMO_COHORT_FILTERS } from "./constants"; +import { DemoText } from "@/components/tailwindComponents"; +import { LoadingOverlay } from "@mantine/core"; +import { + SelectSamples, + SelectSamplesCallBackArg, + SelectSamplesCallback, + RxComponentCallbacks, +} from "./sjpp-types"; +//import { SaveCohortModal } from "@gff/portal-components"; +//import GeneSetModal from "@/components/Modals/SetModals/GeneSetModal"; +import { isEqual, cloneDeep } from "lodash"; +//import { cohortActionsHooks } from "../cohortBuilder/CohortManager/cohortActionHooks"; +//import { INVALID_COHORT_NAMES } from "../cohortBuilder/utils"; +import { COHORT_FILTER_INDEX } from '@/core'; + +const basepath = 'http://localhost:3000' // PROTEINPAINT_API; + +interface PpProps { + chartType: "matrix" | "hierCluster"; + basepath?: string; +} + +// export const demoFilter = Object.freeze({ +// op: "in", +// content: Object.freeze({ +// field: "cases.disease_type", +// value: Object.freeze(["Gliomas"]), +// }), +// }); + +export const MatrixWrapper: FC = (props: PpProps) => { + const isDemoMode = useIsDemoApp(); + const currentCohort = useCoreSelector((state) => + selectCurrentCohortCaseFilters(state, COHORT_FILTER_INDEX), + ); + const filter0 = isDemoMode ? null : buildCohortGqlOperator(currentCohort); + const userDetails = useFetchUserDetailsQuery(); + const prevData = useRef(); + const toolApp = useRef(); + const coreDispatch = useCoreDispatch(); + const [showSaveCohortModal, setShowSaveCohortModal] = useState(false); + const [isLoading, setIsLoading] = useState(false); + // const [createSet, response] = useCreateCaseSetFromValuesMutation(); + // const [newCohortFilters, setNewCohortFilters] = + // useState(undefined); + const [customGeneSetParam, setCustomGeneSetParam] = useState(null); + const [lastGeneSetRequestId, setLastGeneSetRequestId] = useState(undefined); + + const dispatch = useCoreDispatch(); + const modal = useCoreSelector((state) => selectCurrentModal(state)); + + const callback = () => {} + // useCallback( + // (arg: SelectSamplesCallBackArg) => { + // const cases = arg.samples.map((d) => d["cases.case_id"]); + // if (cases.length > 1) { + // createSet({ values: cases, intent: "portal", set_type: "frozen" }); + // } else { + // setNewCohortFilters({ + // mode: "and", + // root: { + // "cases.case_id": { + // operator: "includes", + // field: "cases.case_id", + // operands: cases, + // }, + // }, + // }); + // setShowSaveCohortModal(true); + // } + // }, + // [createSet], + // ); + + // a set for the new cohort is created, now show the save cohort modal + // useDeepCompareEffect(() => { + // if (response.isSuccess) { + // const filters: FilterSet = { + // mode: "and", + // root: { + // "cases.case_id": { + // operator: "includes", + // field: "cases.case_id", + // operands: [`set_id:${response.data}`], + // }, + // }, + // }; + // setNewCohortFilters(filters); + // setShowSaveCohortModal(true); + // } + // }, [response.isSuccess, coreDispatch, response.data]); + + // const genesResponse = useGetGenesQuery( + // { + // request: { + // filters: { + // op: "in", + // content: { + // field: "genes.gene_id", + // value: customGeneSetParam, + // }, + // }, + // fields: ["gene_id", "symbol"], + // size: 1000, + // //from: currentPage * PAGE_SIZE, + // //sortBy, + // }, + // fetchAll: false, + // }, + // { skip: !customGeneSetParam?.length }, + // ); + // const { + // data: geneDetailData, + // isFetching: isGeneFetching, + // requestId: genesRequestId, + // } = genesResponse; + + const showLoadingOverlay = () => setIsLoading(true); + const hideLoadingOverlay = () => setIsLoading(false); + const matrixCallbacks: RxComponentCallbacks = { + "postRender.gdcOncoMatrix": hideLoadingOverlay, + "error.gdcOncoMatrix": hideLoadingOverlay, + }; + const appCallbacks: RxComponentCallbacks = { + "preDispatch.gdcPlotApp": showLoadingOverlay, + "error.gdcPlotApp": hideLoadingOverlay, + "postRender.gdcPlotApp": hideLoadingOverlay, + }; + // const genesetCallback = (/*{callback}*/) => { + // dispatch(showModal({ modal: Modals.LocalGeneSetModal })); + // // TODO: pass the gene set to the callback + // }; + const initArgs = getMatrixTrack( + props, + callback, + matrixCallbacks, + appCallbacks, + //genesetCallback, + ); + + useDeepCompareEffect( + () => { + // debounce until one of these is true + // otherwise, the userDetails.isFetching changing from false > true > false + // could trigger unnecessary, wastefule PP-app state update + if (userDetails?.isSuccess === false && userDetails?.isError === false) + return; + //if (isGeneFetching) return; + const data = { + filter0: filter0 || null, + userData: userDetails?.data, + //geneDetailData, + }; + const hasUpdates = + (data || prevData.current) && !isEqual(prevData.current, data); + if (hasUpdates) prevData.current = data; + const rootElem = divRef.current as HTMLElement; + + let updateArgs; + if (hasUpdates) { + updateArgs = { filter0: data.filter0 }; + // if (lastGeneSetRequestId != genesRequestId) { + // setLastGeneSetRequestId(genesRequestId); + // updateArgs.genes = geneDetailData.hits.map((h) => ({ + // gene: h.symbol, + // })); + // } + + // TODO: + // showing and hiding the overlay should be triggered by components that may take a while to load/render, + // this wrapper code can show the overlay here since it has supplied postRender callbacks above, + // but ideally it is the PP-app that triggers both the showing and hiding of the overlay for reliable behavior + const toolContainer = rootElem?.parentNode?.parentNode?.parentNode as HTMLElement; + toolContainer.style.backgroundColor = "#fff"; + } + + Object.assign(initArgs, { + holder: rootElem, + noheader: true, + nobox: true, + hide_dsHandles: true, + filter0: data.filter0, + }); + + // bindProteinPaint() handles rapid update requests/race condition, + // so no need to include debouncing and promise code in this wrapper + // TODO: will revert to using runproteinpaint() once these advanced capabilities + // are merged into it + bindProteinPaint({ + rootElem, + initArgs, + updateArgs, + isStale() { + // new data has replaced this one, will prevent unnecessary render + // in case of race condition + return prevData.current != data; + }, + }) + .then?.((_app: any) => { + toolApp.current = _app; + }) + .catch((e: any) => { + // the app should either work or display an error in a red banner within the tool container div, + // this uncaught-by-app error is unlikely to happen except for bundling issues that are not detected at build time + console.error(e); + }); + + return () => { + if (!toolApp.current) return; + const toolName = + props.chartType == "hierCluster" ? "GeneExpression" : "OncoMatrix"; + if (window.location.href.includes(toolName)) return; + // cancel unnecessary network requests when this tool app is hidden + toolApp.current.triggerAbort(); + }; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [filter0, userDetails, /*geneDetailData*/], + ); + + const divRef = useRef(null); + + // const updateFilters = (field: string, operation: Operation) => { + // dispatch(hideModal()); + // setCustomGeneSetParam((operation as Includes).operands); + // }; + const existingFiltersHook = () => null; + return ( +
+ {isDemoMode && Showing cases in demo cohort.} +
+ + {/* setShowSaveCohortModal(false)} + filters={newCohortFilters} + hooks={cohortActionsHooks} + invalidCohortNames={INVALID_COHORT_NAMES} + /> + + */} + + +
+ ); +}; + +interface MatrixArg { + dslabel?: string, + genes?: string[], + holder?: HTMLElement; + noheader?: boolean; + nobox?: boolean; + hide_dsHandles?: boolean; + host: string; + launchGdcMatrix: boolean; + launchGdcHierCluster: boolean; + filter0?: any; //FilterSet; + opts: MatrixArgOpts; + state?: any; +} + +interface MatrixArgOpts { + app: MatrixArgOptsApp; + matrix?: MatrixArgOptsMatrix; + hierCluster?: MatrixArgOptsMatrix; +} + +interface MatrixArgOptsApp { + callbacks?: RxComponentCallbacks; +} + +interface MatrixArgOptsMatrix { + allow2selectSamples?: SelectSamples; + callbacks?: RxComponentCallbacks; + customInputs?: { + geneset?: { + label: string; + showInput: () => void; + }[]; + }; +} + +function getMatrixTrack( + props: PpProps, + callback?: SelectSamplesCallback, + matrixCallbacks?: RxComponentCallbacks, + appCallbacks?: RxComponentCallbacks, + //genesetCallback?: () => void, +): MatrixArg { + const arg: MatrixArg = { + dslabel: 'MMRF', + genes: ['MYC', 'MYCN', 'NSD2', 'KRAS'], + // host in gdc is just a relative url path, + // using the same domain as the GDC portal where PP is embedded + host: props.basepath || (basepath as string), + launchGdcMatrix: props.chartType == "matrix", + launchGdcHierCluster: props.chartType == "hierCluster", + opts: { + app: { + callbacks: appCallbacks, + }, + [props.chartType]: { + allow2selectSamples: { + buttonText: "Create Cohort", + attributes: [ + { + from: "sample", + to: "cases.case_id", + convert: true, + }, + ], + callback, + }, + callbacks: matrixCallbacks, + // customInputs: { + // geneset: [ + // { + // label: "Load Gene Sets", + // showInput: genesetCallback, + // }, + // ], + // }, + }, + }, + }; + + return arg; +} diff --git a/src/features/proteinpaint/ProteinPaintWrapper.tsx b/src/features/proteinpaint/ProteinPaintWrapper.tsx new file mode 100644 index 0000000..8b5d81a --- /dev/null +++ b/src/features/proteinpaint/ProteinPaintWrapper.tsx @@ -0,0 +1,239 @@ +import React, { useRef, useCallback, useState, FC } from "react"; +import { useDeepCompareEffect } from "use-deep-compare"; +import { bindProteinPaint } from "@sjcrh/proteinpaint-client"; +import { useIsDemoApp } from "@/hooks/useIsDemoApp"; +import { + useCoreSelector, + FilterSet, + //PROTEINPAINT_API, + useFetchUserDetailsQuery, + useCoreDispatch, + convertFilterSetToGqlFilter as buildCohortGqlOperator +} from "@gen3/core"; +import { isEqual, cloneDeep } from "lodash"; +import { DemoText } from "@/components/tailwindComponents"; +import { joinFilters, selectCurrentCohortCaseFilters } from "@/core/utils"; +import { DEMO_COHORT_FILTERS } from "./constants"; +// import { SaveCohortModal } from "@gff/portal-components"; +import { + SelectSamples, + SelectSamplesCallBackArg, + SelectSamplesCallback, +} from "./sjpp-types"; +// import { cohortActionsHooks } from "../cohortBuilder/CohortManager/cohortActionHooks"; +// import { INVALID_COHORT_NAMES } from "../cohortBuilder/utils"; +import { COHORT_FILTER_INDEX } from '@/core'; + +const basepath = 'http://localhost:3000'; //PROTEINPAINT_API; + +interface PpProps { + basepath?: string; + geneId?: string; + gene2canonicalisoform?: string; + ssm_id?: string; + mds3_ssm2canonicalisoform?: mds3_isoform; + geneSearch4GDCmds3?: boolean; + hardcodeCnvOnly?: boolean; +} + +export const ProteinPaintWrapper: FC = (props: PpProps) => { + const isDemoMode = useIsDemoApp(); + const currentCohort = useCoreSelector((state) => + selectCurrentCohortCaseFilters(state, COHORT_FILTER_INDEX), + ); + const filter0 = isDemoMode ? null : buildCohortGqlOperator(currentCohort); + const userDetails = useFetchUserDetailsQuery(); + + // to track reusable instance for mds3 skewer track + const prevArg = useRef({}); + const coreDispatch = useCoreDispatch(); + const [showSaveCohort, setShowSaveCohort] = useState(false); + //const [createSet, response] = useCreateCaseSetFromValuesMutation(); + // const [newCohortFilters, setNewCohortFilters] = + // useState(undefined); + + const callback = () => {} + // useCallback( + // (arg: SelectSamplesCallBackArg) => { + // const cases = arg.samples.map((d) => d["cases.case_id"]); + // if (cases.length > 1) { + // createSet({ values: cases, intent: "portal", set_type: "frozen" }); + // } else { + // setNewCohortFilters({ + // mode: "and", + // root: { + // "cases.case_id": { + // operator: "includes", + // field: "cases.case_id", + // operands: cases, + // }, + // }, + // }); + // setShowSaveCohort(true); + // } + // }, + // [createSet], + // ); + + // a set for the new cohort is created, now show the save cohort modal + // useDeepCompareEffect(() => { + // if (response.isSuccess) { + // const filters: FilterSet = { + // mode: "and", + // root: { + // "cases.case_id": { + // operator: "includes", + // field: "cases.case_id", + // operands: [`set_id:${response.data}`], + // }, + // }, + // }; + // setNewCohortFilters(filters); + // setShowSaveCohort(true); + // } + // }, [response.isSuccess, coreDispatch, response.data]); + + useDeepCompareEffect( + () => { + const rootElem = divRef.current; + const data = getLollipopTrack(props, filter0, callback); + if (!data) return; + if (isDemoMode) { + data.geneSymbol = props.hardcodeCnvOnly + ? "chr8:127682515-127792250" + : "MYC"; + } + // compare the argument to runpp to avoid unnecessary render + if ((data || prevArg.current) && isEqual(prevArg.current, data)) return; + prevArg.current = data; + + const toolContainer = rootElem?.parentNode?.parentNode?.parentNode as HTMLElement; + if (!toolContainer) return + toolContainer.style.backgroundColor = "#fff"; + + const arg = Object.assign( + { holder: rootElem, noheader: true, nobox: true }, + cloneDeep(data), + ) as Mds3Arg; + + // bindProteinPaint() handles rapid update requests/race condition, + // so no need to include debouncing and promise code in this wrapper + // TODO: will revert to using runproteinpaint() once these advanced capabilities + // are merged into it + bindProteinPaint({ + rootElem, + initArgs: arg, + updateArgs: arg, + isStale() { + // new data has replaced this one, will prevent unnecessary render + // in case of race condition + return prevArg.current != data; + }, + }); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + props.gene2canonicalisoform, + props.mds3_ssm2canonicalisoform, + props.geneSearch4GDCmds3, + isDemoMode, + filter0, + userDetails, + ], + ); + + const divRef = useRef(null); + const demoText = props.hardcodeCnvOnly + ? "Demo showing MYC CNV segments from all GDC cases" + : "Demo showing MYC mutations from all GDC cases."; + return ( +
+ {isDemoMode && {demoText}} +
+ + {/* setShowSaveCohort(false)} + filters={newCohortFilters} + hooks={cohortActionsHooks} + invalidCohortNames={INVALID_COHORT_NAMES} + />*/} +
+ ); +}; + +interface Mds3Arg { + dslabel?: string; + holder?: HTMLElement; + noheader?: boolean; + nobox?: boolean; + hide_dsHandles?: boolean; + host: string; + gene2canonicalisoform?: string; + mds3_ssm2canonicalisoform?: mds3_isoform; + geneSearch4GDCmds3?: + | boolean + | { + hardcodeCnvOnly?: boolean; + }; + geneSymbol?: string; + tracks?: Track[]; + filter0?: FilterSet; + allow2selectSamples?: SelectSamples; +} + +interface Track { + type: string; + dslabel: string; + filter0: FilterSet; + allow2selectSamples?: SelectSamples; + hardcodeCnvOnly?: boolean; +} + +interface mds3_isoform { + ssm_id: string; + dslabel: string; +} + +function getLollipopTrack( + props: PpProps, + filter0: any, + callback: SelectSamplesCallback, +) { + const arg: Mds3Arg = { + dslabel: 'MMRF', + // host in gdc is just a relative url path, + // using the same domain as the GDC portal where PP is embedded + host: props.basepath || (basepath as string), + geneSearch4GDCmds3: { + hardcodeCnvOnly: props.hardcodeCnvOnly, + }, + filter0, + allow2selectSamples: { + buttonText: "Create Cohort", + attributes: [{ from: "sample_id", to: "cases.case_id", convert: true }], + callback, + }, + }; + + if (props.hardcodeCnvOnly) { + arg.geneSearch4GDCmds3 = { + hardcodeCnvOnly: true, + }; + } else if (props.geneId) { + arg.gene2canonicalisoform = props.geneId; + } else if (props.ssm_id) { + arg.mds3_ssm2canonicalisoform = { + dslabel: "MMRF", + ssm_id: props.ssm_id, + }; + } else { + arg.geneSearch4GDCmds3 = true; + } + + return arg; +} diff --git a/src/features/proteinpaint/constants.ts b/src/features/proteinpaint/constants.ts new file mode 100644 index 0000000..9478ba8 --- /dev/null +++ b/src/features/proteinpaint/constants.ts @@ -0,0 +1,12 @@ +import { FilterSet } from "@gen3/core"; + +export const DEMO_COHORT_FILTERS: FilterSet = { + mode: "and", + root: { + "cases.project.project_id": { + operator: "includes", + field: "cases.project.project_id", + operands: ["TCGA-LGG"], + }, + }, +}; diff --git a/src/features/proteinpaint/dev.sh b/src/features/proteinpaint/dev.sh new file mode 100755 index 0000000..336d87f --- /dev/null +++ b/src/features/proteinpaint/dev.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# run from the gdc-frontend-framework project root folder +# ./packages/portal-proto/src/features/proteinpaint/dev.sh +# assumes that the proteinpaint folder is a sibling dir of gff +if [[ "$1" == "unlink" ]]; then + # to test the published client package before submitting a PR with an updated pp-client version + npm unlink ../proteinpaint/client + npm uninstall @sjcrh/proteinpaint-client --save --workspace=packages/portal-proto + npm install @sjcrh/proteinpaint-client --save --save-exact --workspace=packages/portal-proto + +else + # to test the local PP client code + npm link ../proteinpaint/client + # An issue with npm link and workspaces: the non-linked @sjcrh/proteinpaint-client package + # may be moved to portal-proto/node_modules, creating 2 separate modules of the same package, + # must ensure only the linked module is used for bundling so delete + rm -rf packages/portal-proto/node_modules/@sjcrh + # also not able to do a simpler + # `cd packages/portal-proto && npm link path/to/proteinpaint/client`, + # where the linked module would be in portal-proto/node_modules instead of the + # other way around +fi + +# sometimes the nextjs bundle cache are stale after npm link +# rm -rf packages/portal-proto/.next + +# run the following tab in a separate tab +# local-ssl-proxy --config ssl-proxy.json --cert localhost.pem --key localhost-key.pem +# then from the gff dir +PROTEINPAINT_API="http://localhost:3000" PORT=3333 npm run dev +# +# OR start close all open Chrome browser windows and in macOS terminal: +# open -n /Applications/Google\ Chrome.app --args --user-data-dir="/tmp/chrome-dev-session" --disable-web-security +# +# TODO: setup and use https://localhost.dev-virtuallab.themmrf.org using local-ssl-proxy +# diff --git a/src/features/proteinpaint/sjpp-types.tsx b/src/features/proteinpaint/sjpp-types.tsx new file mode 100644 index 0000000..af29376 --- /dev/null +++ b/src/features/proteinpaint/sjpp-types.tsx @@ -0,0 +1,56 @@ +import { FilterSet } from "@gen3/core"; + +export interface PpApi { + update(arg: any): null; + staleInstance?: boolean; + getState?: () => any; +} + +export type SampleData = { + "cases.case_id": string; +}; + +export interface SelectSamplesCallBackArg { + samples: SampleData[]; + source: string; +} + +export type SelectSamplesCallback = (samples: SelectSamplesCallBackArg) => void; + +export type attrMapping = { + from: string; + to?: string; + convert?: boolean; +}; + +export interface SelectSamples { + buttonText: string; + attributes: attrMapping[]; + callback?: SelectSamplesCallback; +} + +export function getFilters(arg: SelectSamplesCallBackArg): FilterSet { + const { samples } = arg; + // see comments below about SV-2228 + const ids: (string | number)[] = samples.map((d) => d["cases.case_id"]).filter((d) => typeof d === 'string' || typeof d === 'number'); + return { + mode: "and", + root: { + // see https://jira.opensciencedatacloud.org/browse/SV-2228 + // per Craig: I suggest always representing cohorts comprised of cases using the cases.case_id filter, + // as it is the most general of the case filters. Within an app, you can use any filter needed; + // when these become part of the cohort, there is some potential for issues. + // NOTE: This is primarily because both the frontend and backends are trying to, in effect, + // JOINs across the explore and repository indexes, and there is some additional work needed to work will all filters. + "cases.case_id": { + operator: "includes", + field: "cases.case_id", + operands: ids, + }, + }, + }; +} + +export type RxComponentCallbacks = { + [eventName: string]: () => void; +};