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;
+};