diff --git a/app/common.d.ts b/app/common.d.ts index e3452ea02..a149d8cb2 100644 --- a/app/common.d.ts +++ b/app/common.d.ts @@ -156,6 +156,7 @@ type CopyNumberType = { kbMatches?: KbMatchType<'cnv'>[]; log2Cna: string | null; lohState: string | null; + selected: boolean; size: number | null; start: number | null; variantType: 'cnv'; @@ -182,6 +183,7 @@ type StructuralVariantType = { ntermGene: string | null; ntermTranscript: string | null; omicSupport: boolean; + selected: boolean; svg: string | null; svgTitle: string | null; variantType: 'sv'; @@ -209,6 +211,7 @@ type SmallMutationType = { rnaAltCount: number | null; rnaDepth: number | null; rnaRefCount: number | null; + selected: boolean; startPosition: number | null; transcript: string | null; tumourAltCopies: number | null; @@ -242,11 +245,14 @@ type ExpOutliersType = { primarySitekIQR: number | null; rnaReads: number | null; rpkm: number | null; + selected: boolean; tpm: number | null; variantType: 'exp'; } & RecordDefaults; type TmburType = { + adjustedTmb: number | null; + adjustedTmbComment: string | null; cdsBasesIn1To22AndXAndY: string; cdsIndels: number; cdsIndelTmb: number; @@ -255,8 +261,6 @@ type TmburType = { comments: string; genomeSnvTmb: number; genomeIndelTmb: number; - adjustedTmb: number | null; - adjustedTmbComment: string | null; tmbHidden: boolean; kbCategory: string | null; kbMatches: KbMatchType[]; diff --git a/app/components/VariantEditDialog/index.scss b/app/components/VariantEditDialog/index.scss new file mode 100644 index 000000000..a76c1aea1 --- /dev/null +++ b/app/components/VariantEditDialog/index.scss @@ -0,0 +1,6 @@ +.variant-edit-dialog { + &__form-control { + min-width: 160px; + margin: 0 0 16px; + } + } \ No newline at end of file diff --git a/app/components/VariantEditDialog/index.tsx b/app/components/VariantEditDialog/index.tsx new file mode 100644 index 000000000..396ea1241 --- /dev/null +++ b/app/components/VariantEditDialog/index.tsx @@ -0,0 +1,140 @@ +import React, { + useState, useEffect, useContext, useCallback, +} from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, +} from '@mui/material'; + +import api from '@/services/api'; +import AsyncButton from '@/components/AsyncButton'; + +import ConfirmContext from '@/context/ConfirmContext'; +import ReportContext from '@/context/ReportContext'; +import snackbar from '@/services/SnackbarUtils'; +import useConfirmDialog from '@/hooks/useConfirmDialog'; +import { + CopyNumberType, SmallMutationType, StructuralVariantType, ExpOutliersType, +} from '@/common'; +import { GeneVariantType } from '@/views/ReportView/components/GenomicSummary/types'; + +import './index.scss'; + + type VariantEditDialogProps = { + editData: SmallMutationType | CopyNumberType | StructuralVariantType | ExpOutliersType; + variantType: string; + isOpen: boolean; + onClose: (newData?: SmallMutationType | CopyNumberType | StructuralVariantType | ExpOutliersType) => void; + showErrorSnackbar: (message: string) => void; + }; + +const VariantEditDialog = ({ + editData, + variantType, + isOpen = false, + onClose, + showErrorSnackbar, +}: VariantEditDialogProps): JSX.Element => { + const { showConfirmDialog } = useConfirmDialog(); + const { report } = useContext(ReportContext); + const { isSigned } = useContext(ConfirmContext); + const [variant, setVariant] = useState(''); + const [variants, setVariants] = useState(); + const [isApiCalling, setIsApiCalling] = useState(false); + + useEffect(() => { + if (editData) { + let newVariant; + switch (variantType) { + case 'snv': + newVariant = `${editData?.gene.name}:${editData?.proteinChange}`; + setVariant(newVariant); + break; + case 'cnv': + newVariant = `${editData?.gene.name} (${editData?.cnvState})`; + setVariant(newVariant); + break; + case 'sv': + newVariant = `${editData?.displayName}`; + setVariant(newVariant); + break; + case 'exp': + newVariant = `${editData?.gene.name} (${editData?.expressionState})`; + setVariant(newVariant); + break; + default: + break; + } + } + }, [editData, variantType]); + + useEffect(() => { + async function fetchVariants() { + const variantsResp = api.get(`/reports/${report.ident}/summary/genomic-alterations-identified`).request(); + if (variantsResp) { + setVariants(await variantsResp); + } + } + + fetchVariants(); + }, [report.ident]); + + const availableVariants = variants?.map(({ geneVariant }) => geneVariant); + + const handleSubmit = useCallback(async () => { + if (!availableVariants.includes(variant)) { + setIsApiCalling(true); + const req = api.post(`/reports/${report.ident}/summary/genomic-alterations-identified`, { geneVariant: variant }); + try { + if (isSigned) { + showConfirmDialog(req); + setIsApiCalling(false); + } else { + await req.request(); + onClose({ ...editData }); + snackbar.success('Variant added to key alterations.'); + } + } catch (err) { + showErrorSnackbar(`Error updating key alterations: ${err.message}`); + onClose(); + } finally { + setIsApiCalling(false); + } + } else { + snackbar.error('Variant already in key alterations.'); + onClose(); + } + }, [availableVariants, variant, report.ident, isSigned, showConfirmDialog, onClose, editData, showErrorSnackbar]); + + const handleClose = useCallback(() => { + onClose(); + }, [onClose]); + + return ( + + Key Alterations Edit + +
+

+ Variant: + {' '} + {variant} +

+
+ + + Add to Summary + + + +
+
+ ); +}; + +export default VariantEditDialog; diff --git a/app/hooks/useConfirmDialog.tsx b/app/hooks/useConfirmDialog.tsx index b9c2e98ab..35839e0e5 100644 --- a/app/hooks/useConfirmDialog.tsx +++ b/app/hooks/useConfirmDialog.tsx @@ -38,7 +38,7 @@ const useConfirmDialog = () => { title="Confirm Action" text={textDict[report?.template.name]} confirmText="Yes" - cancelText="cancel" + cancelText="Cancel" />, document.getElementById('alert-dialog'), ); diff --git a/app/views/ProjectsView/columnDefs.ts b/app/views/ProjectsView/columnDefs.ts index 27f545089..6fb48d931 100644 --- a/app/views/ProjectsView/columnDefs.ts +++ b/app/views/ProjectsView/columnDefs.ts @@ -16,7 +16,7 @@ const readOnlyColDefs = [ }, { headerName: 'Number of reports', - valueGetter: ({ data }) => data?.reports?.length, + valueGetter: ({ data }) => Number(data?.reportCount), }, { headerName: 'Number of users', diff --git a/app/views/ProjectsView/index.tsx b/app/views/ProjectsView/index.tsx index eb4de48db..35808f7c7 100644 --- a/app/views/ProjectsView/index.tsx +++ b/app/views/ProjectsView/index.tsx @@ -26,14 +26,14 @@ const Projects = (): JSX.Element => { useEffect(() => { const getData = async () => { - const projectsResp = await api.get('/project?admin=true').request(); + const projectsResp = await api.get(`/project?admin=${adminAccess}`).request(); setProjects(projectsResp); setLoading(false); }; getData(); - }, []); + }, [adminAccess]); const handleEditStart = (rowData) => { setShowDialog(true); @@ -41,7 +41,7 @@ const Projects = (): JSX.Element => { }; const handleDelete = useCallback(async ({ ident }) => { - // eslint-disable-next-line no-restricted-globals + // eslint-disable-next-line no-restricted-globals, no-alert if (confirm('Are you sure you want to remove this project?')) { await api.del(`/project/${ident}`, {}).request(); const newProjects = projects.filter((project) => project.ident !== ident); diff --git a/app/views/ReportView/components/CopyNumber/index.tsx b/app/views/ReportView/components/CopyNumber/index.tsx index fcd91513b..e92f1614d 100644 --- a/app/views/ReportView/components/CopyNumber/index.tsx +++ b/app/views/ReportView/components/CopyNumber/index.tsx @@ -8,6 +8,7 @@ import { import DataTable from '@/components/DataTable'; import ReportContext from '@/context/ReportContext'; +import useReport from '@/hooks/useReport'; import api, { ApiCallSet } from '@/services/api'; import { CNVSTATE, EXPLEVEL } from '@/constants'; import Image from '@/components/Image'; @@ -15,6 +16,7 @@ import ImageType from '@/components/Image/types'; import snackbar from '@/services/SnackbarUtils'; import { CopyNumberType } from '@/common'; import withLoading, { WithLoadingInjectedProps } from '@/hoc/WithLoading'; +import VariantEditDialog from '@/components/VariantEditDialog'; import columnDefs from './columnDefs'; import './index.scss'; @@ -50,6 +52,7 @@ const CopyNumber = ({ setIsLoading, }: CopyNumberProps): JSX.Element => { const { report } = useContext(ReportContext); + const { canEdit } = useReport(); const theme = useTheme(); const [images, setImages] = useState([]); const [circos, setCircos] = useState(); @@ -70,6 +73,8 @@ const CopyNumber = ({ } return accumulator; }, []), ); + const [showDialog, setShowDialog] = useState(false); + const [editData, setEditData] = useState(); useEffect(() => { if (report) { @@ -187,6 +192,16 @@ const CopyNumber = ({ maxHeight: `calc(100vh - ${theme.mixins.toolbar.minHeight as number * 3}px)`, }), [theme.mixins?.toolbar?.minHeight]); + const handleEditStart = (rowData: CopyNumberType) => { + setShowDialog(true); + setEditData(rowData); + }; + + const handleEditClose = () => { + setShowDialog(false); + setEditData(null); + }; + const handleVisibleColsChange = (change) => setVisibleCols(change); return ( @@ -195,6 +210,15 @@ const CopyNumber = ({ {!isLoading && ( <> Summary of Copy Number Events + {showDialog && ( + + )} {circos ? (
))} diff --git a/app/views/ReportView/components/Expression/index.tsx b/app/views/ReportView/components/Expression/index.tsx index 32e296a14..b3039d9e0 100644 --- a/app/views/ReportView/components/Expression/index.tsx +++ b/app/views/ReportView/components/Expression/index.tsx @@ -4,10 +4,14 @@ import React, { import { Typography, Paper } from '@mui/material'; import DataTable from '@/components/DataTable'; import api from '@/services/api'; +import snackbar from '@/services/SnackbarUtils'; import DemoDescription from '@/components/DemoDescription'; import ReportContext from '@/context/ReportContext'; +import useReport from '@/hooks/useReport'; import withLoading, { WithLoadingInjectedProps } from '@/hoc/WithLoading'; import { ImageType } from '@/components/Image'; +import { ExpOutliersType } from '@/common'; +import VariantEditDialog from '@/components/VariantEditDialog'; import columnDefs from './columnDefs'; import processExpression from './processData'; import { @@ -59,6 +63,7 @@ const Expression = ({ setIsLoading, }: ExpressionProps): JSX.Element => { const { report } = useContext(ReportContext); + const { canEdit } = useReport(); const [tissueSites, setTissueSites] = useState(); const [comparators, setComparators] = useState(); const [expOutliers, setExpOutliers] = useState(); @@ -66,6 +71,9 @@ const Expression = ({ columnDefs.reduce(getVisibleColsFromColDefReducer, []), ); + const [showDialog, setShowDialog] = useState(false); + const [editData, setEditData] = useState(); + useEffect(() => { if (report && report.ident) { const getData = async () => { @@ -159,6 +167,16 @@ const Expression = ({ } }, [report]); + const handleEditStart = (rowData: ExpOutliersType) => { + setShowDialog(true); + setEditData(rowData); + }; + + const handleEditClose = () => { + setShowDialog(false); + setEditData(null); + }; + return (!isLoading && ( <>
@@ -218,6 +236,15 @@ const Expression = ({ No comparator data to display )}
+ {showDialog && ( + + )} {expOutliers && (Object.entries(TITLE_MAP).map(([key, titleText]) => ( )))} diff --git a/app/views/ReportView/components/MutationSignatures/index.tsx b/app/views/ReportView/components/MutationSignatures/index.tsx index 1f9954bf7..31f8e73d0 100644 --- a/app/views/ReportView/components/MutationSignatures/index.tsx +++ b/app/views/ReportView/components/MutationSignatures/index.tsx @@ -49,9 +49,9 @@ const MutationSignatures = ({ api.get(`/reports/${report.ident}/mutation-signatures`).request(), ]); setImages(imageData); - setSbsSignatures(signatureData.filter((sig) => !(new RegExp(/dbs|id/)).test(sig.signature.toLowerCase()))); - setDbsSignatures(signatureData.filter((sig) => (new RegExp(/dbs/)).test(sig.signature.toLowerCase()))); - setIdSignatures(signatureData.filter((sig) => (new RegExp(/id/)).test(sig.signature.toLowerCase()))); + setSbsSignatures(signatureData.filter((sig) => !(/dbs|id/).test(sig.signature.toLowerCase()))); + setDbsSignatures(signatureData.filter((sig) => (/dbs/).test(sig.signature.toLowerCase()))); + setIdSignatures(signatureData.filter((sig) => (/id/).test(sig.signature.toLowerCase()))); } catch (err) { snackbar.error(`Network error: ${err}`); } finally { diff --git a/app/views/ReportView/components/SmallMutations/index.tsx b/app/views/ReportView/components/SmallMutations/index.tsx index c6dc76554..3bd15f5af 100644 --- a/app/views/ReportView/components/SmallMutations/index.tsx +++ b/app/views/ReportView/components/SmallMutations/index.tsx @@ -7,8 +7,10 @@ import api from '@/services/api'; import snackbar from '@/services/SnackbarUtils'; import DataTable from '@/components/DataTable'; import ReportContext from '@/context/ReportContext'; +import useReport from '@/hooks/useReport'; import { SmallMutationType } from '@/common'; import withLoading, { WithLoadingInjectedProps } from '@/hoc/WithLoading'; +import VariantEditDialog from '@/components/VariantEditDialog'; import { columnDefs } from './columnDefs'; import './index.scss'; @@ -36,6 +38,7 @@ const SmallMutations = ({ setIsLoading, }: SmallMutationsProps): JSX.Element => { const { report } = useContext(ReportContext); + const { canEdit } = useReport(); const [smallMutations, setSmallMutations] = useState([]); const [groupedSmallMutations, setGroupedSmallMutations] = useState({ therapeutic: [], @@ -51,6 +54,9 @@ const SmallMutations = ({ }, []), ); + const [showDialog, setShowDialog] = useState(false); + const [editData, setEditData] = useState(); + useEffect(() => { if (report) { const getData = async () => { @@ -68,6 +74,7 @@ const SmallMutations = ({ for (const { gene: { expressionVariants: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars tpm, rpkm, primarySiteFoldChange, }, }, @@ -132,6 +139,16 @@ const SmallMutations = ({ } }, [smallMutations]); + const handleEditStart = (rowData: SmallMutationType) => { + setShowDialog(true); + setEditData(rowData); + }; + + const handleEditClose = () => { + setShowDialog(false); + setEditData(null); + }; + const handleVisibleColsChange = (change) => setVisibleCols(change); const dataTables = useMemo(() => Object.entries(groupedSmallMutations).map(([key, value]) => ( @@ -144,15 +161,30 @@ const SmallMutations = ({ syncVisibleColumns={handleVisibleColsChange} visibleColumns={visibleCols} demoDescription={INFO_BUBBLES[key]} + canEdit={canEdit} + onEdit={handleEditStart} /> - )), [groupedSmallMutations, visibleCols]); + )), [canEdit, groupedSmallMutations, visibleCols]); return (
- - Small Mutations - - { !isLoading ? dataTables : null } + {!isLoading && ( + <> + + Small Mutations + + {showDialog && ( + + )} + { dataTables } + + )}
); }; diff --git a/app/views/ReportView/components/StructuralVariants/index.tsx b/app/views/ReportView/components/StructuralVariants/index.tsx index 09eea52db..82b7bd2e8 100644 --- a/app/views/ReportView/components/StructuralVariants/index.tsx +++ b/app/views/ReportView/components/StructuralVariants/index.tsx @@ -13,9 +13,11 @@ import snackbar from '@/services/SnackbarUtils'; import DataTable from '@/components/DataTable'; import Image from '@/components/Image'; import ReportContext from '@/context/ReportContext'; +import useReport from '@/hooks/useReport'; import ImageType from '@/components/Image/types'; import { StructuralVariantType } from '@/common'; import withLoading, { WithLoadingInjectedProps } from '@/hoc/WithLoading'; +import VariantEditDialog from '@/components/VariantEditDialog'; import columnDefs from './columnDefs'; import './index.scss'; @@ -43,6 +45,7 @@ const StructuralVariants = ({ setIsLoading, }: StructuralVariantsProps): JSX.Element => { const { report } = useContext(ReportContext); + const { canEdit } = useReport(); const theme = useTheme(); const [svs, setSvs] = useState([]); const [groupedSvs, setGroupedSvs] = useState({ @@ -63,6 +66,9 @@ const StructuralVariants = ({ }, []), ); + const [showDialog, setShowDialog] = useState(false); + const [editData, setEditData] = useState(); + useEffect(() => { if (report) { const getData = async () => { @@ -161,6 +167,16 @@ const StructuralVariants = ({ setTabIndex(newValue); }; + const handleEditStart = (rowData: StructuralVariantType) => { + setShowDialog(true); + setEditData(rowData); + }; + + const handleEditClose = () => { + setShowDialog(false); + setEditData(null); + }; + const handleVisibleColsChange = (change) => setVisibleCols(change); return ( @@ -171,6 +187,15 @@ const StructuralVariants = ({ Summary of Structural Events + {showDialog && ( + + )} {(genomeCircos || transcriptomeCircos) ? ( <> @@ -215,6 +240,8 @@ const StructuralVariants = ({ syncVisibleColumns={handleVisibleColsChange} canToggleColumns demoDescription={INFO_BUBBLES[key]} + canEdit={canEdit} + onEdit={handleEditStart} /> ))}