Skip to content

Commit 7d1f4b5

Browse files
Merge pull request #809 from bcgsc/feat/DEVSU-2556-key-alterations-table-redesign
DEVSU-2556 Genomic Alterations Table Redesign
2 parents b76d6cb + 212fde2 commit 7d1f4b5

14 files changed

Lines changed: 1305 additions & 243 deletions

File tree

app/common.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ type GeneType = {
155155
kbStatementRelated: boolean;
156156
drugTargetable: boolean;
157157
expressionVariants?: ExpOutliersType;
158+
copyVariants?: CopyNumberType;
158159
knownFusionPartner: boolean;
159160
knownSmallMutation: boolean;
160161
name: string;

app/components/PrintTable/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import { ColDef, ValueGetterParams } from '@ag-grid-community/core';
22

3+
const getNestedValue = (row: Record<string, unknown>, path: string) => path
4+
.split('.')
5+
.reduce<unknown>((currentValue, key) => {
6+
if (currentValue === null || currentValue === undefined) {
7+
return undefined;
8+
}
9+
if (typeof currentValue !== 'object') {
10+
return undefined;
11+
}
12+
return (currentValue as Record<string, unknown>)[key];
13+
}, row);
14+
315
// Resolves the displayed value of a cell for a given row + colDef, applying
416
// the colDef's valueGetter when present and falling back to row[colId|field].
517
// Shared by PrintTable's row-rendering / collapse-key logic and any caller
@@ -13,5 +25,11 @@ export const resolveCellValue = <T extends Record<string, unknown>>(
1325
return colDef.valueGetter({ data: row } as ValueGetterParams);
1426
}
1527
const key = (colDef.field ?? colDef.colId) as string;
28+
if (!key) {
29+
return undefined;
30+
}
31+
if (key.includes('.')) {
32+
return getNestedValue(row, key);
33+
}
1634
return row[key];
1735
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React from 'react';
2+
import {
3+
render,
4+
screen,
5+
waitFor,
6+
} from '@testing-library/react';
7+
import { ACTIONS_COLUMN } from '@/utils/actionsColumnDef';
8+
import { SmallMutationType, ExpOutliersType } from '@/common';
9+
10+
import GenomicAlterationsTable from '..';
11+
12+
const mockDataTable = jest.fn();
13+
const mockPrintTable = jest.fn();
14+
15+
jest.mock('@/components/DataTable', () => (props) => {
16+
mockDataTable(props);
17+
return <div data-testid="data-table">{props.titleText || 'data-table'}</div>;
18+
});
19+
20+
jest.mock('@/components/PrintTable', () => (props) => {
21+
mockPrintTable(props);
22+
return <div data-testid="print-table">print-table</div>;
23+
});
24+
25+
describe('GenomicAlterationsTable', () => {
26+
beforeEach(() => {
27+
mockDataTable.mockClear();
28+
mockPrintTable.mockClear();
29+
});
30+
31+
test('renders DataTable for smallMutation in non-print mode', async () => {
32+
render(
33+
<GenomicAlterationsTable
34+
variantCategory="smallMutation"
35+
variantData={[{ ident: 'v1', gene: { name: 'TP53' } } as SmallMutationType]}
36+
isPrint={false}
37+
/>,
38+
);
39+
40+
expect(await screen.findByTestId('data-table')).toBeInTheDocument();
41+
expect(screen.getByText('Small Mutations')).toBeInTheDocument();
42+
43+
expect(mockDataTable).toHaveBeenCalled();
44+
const dataTableProps = mockDataTable.mock.calls[0][0];
45+
expect(dataTableProps.isPrint).toBe(false);
46+
expect(dataTableProps.canExport).toBe(true);
47+
expect(dataTableProps.titleText).toBe('Small Mutations');
48+
49+
expect(mockPrintTable).not.toHaveBeenCalled();
50+
});
51+
52+
test('renders PrintTable for smallMutation in print mode', async () => {
53+
render(
54+
<GenomicAlterationsTable
55+
variantCategory="smallMutation"
56+
variantData={[{ ident: 'v1', gene: { name: 'TP53' } } as SmallMutationType]}
57+
isPrint
58+
/>,
59+
);
60+
61+
expect(await screen.findByTestId('print-table')).toBeInTheDocument();
62+
expect(screen.getByText('Small Mutations')).toBeInTheDocument();
63+
64+
expect(mockPrintTable).toHaveBeenCalled();
65+
expect(mockDataTable).not.toHaveBeenCalled();
66+
});
67+
68+
test('uses flattened expression print columns and excludes Actions', async () => {
69+
render(
70+
<GenomicAlterationsTable
71+
variantCategory="expression"
72+
variantData={[{ ident: 'v1', gene: { name: 'EGFR', copyVariants: { cnvState: 'gain' } } } as ExpOutliersType]}
73+
isPrint
74+
/>,
75+
);
76+
77+
await screen.findByTestId('print-table');
78+
79+
const printTableProps = mockPrintTable.mock.calls[0][0];
80+
const printHeaders = printTableProps.columnDefs.map((col) => col.headerName);
81+
82+
expect(printHeaders).toContain('Gene');
83+
expect(printHeaders).toContain('Expression Class');
84+
expect(printHeaders).toContain('Disease Perc');
85+
expect(printHeaders).toContain('Disease Z-Score');
86+
expect(printHeaders).not.toContain(ACTIONS_COLUMN);
87+
expect(printTableProps.fullWidth).toBe(true);
88+
});
89+
90+
test('shows loader and no tables for unknown variantCategory', async () => {
91+
render(
92+
<GenomicAlterationsTable
93+
variantCategory="unknown"
94+
variantData={[]}
95+
isPrint={false}
96+
/>,
97+
);
98+
99+
await waitFor(() => {
100+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
101+
});
102+
103+
expect(mockDataTable).not.toHaveBeenCalled();
104+
expect(mockPrintTable).not.toHaveBeenCalled();
105+
});
106+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// eslint-disable-next-line import/no-extraneous-dependencies
2+
import { ColDef, ColGroupDef } from '@ag-grid-community/core';
3+
import { createGeneRelatedValueGetter } from '@/views/ReportView/components/StructuralVariants/columnDefs';
4+
import { actionsColDef } from '@/utils/actionsColumnDef';
5+
6+
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
7+
8+
const smallMutationsColumnDefs: ColDef[] = [{
9+
headerName: 'Gene',
10+
field: 'gene.name',
11+
cellRenderer: 'GeneCellRenderer',
12+
cellRendererParams: { link: true },
13+
},
14+
{
15+
headerName: 'Protein Change',
16+
field: 'proteinChange',
17+
},
18+
{
19+
headerName: 'Transcript',
20+
field: 'transcript',
21+
},
22+
{
23+
headerName: 'Location',
24+
field: 'location',
25+
valueGetter: ({ data }) => data.chromosome && `${data.chromosome}:${data.startPosition}${data.endPosition && data.startPosition !== data.endPosition
26+
? `-${data.endPosition}`
27+
: ''
28+
}`,
29+
},
30+
{
31+
headerName: 'Zygosity',
32+
field: 'zygosity',
33+
},
34+
{
35+
headerName: 'VAF %',
36+
colId: 'tumourAltCount/tumourDepth',
37+
field: 'tumourAltCount/tumourDepth',
38+
valueGetter: ({
39+
data: {
40+
tumourAltCount, tumourDepth, rnaAltCount, rnaDepth,
41+
},
42+
}) => {
43+
if ((tumourAltCount && tumourDepth) || (tumourAltCount === 0 || tumourDepth === 0)) {
44+
return ((tumourAltCount / tumourDepth) * 100).toFixed(0);
45+
}
46+
if ((rnaAltCount && rnaDepth) || (rnaAltCount === 0 || rnaDepth === 0)) {
47+
return 'N/A (RNA)';
48+
}
49+
return '';
50+
},
51+
comparator: collator.compare,
52+
},
53+
{
54+
...actionsColDef,
55+
}];
56+
57+
const copyNumberColumnDefs: ColDef[] = [{
58+
headerName: 'Gene',
59+
cellRenderer: 'GeneCellRenderer',
60+
cellRendererParams: { link: true },
61+
field: 'gene.name',
62+
},
63+
{
64+
headerName: 'Copy Change',
65+
field: 'copyChange',
66+
valueFormatter: (params) => {
67+
if (params.value === null || params.value === undefined) return '';
68+
69+
const num = Number(params.value);
70+
71+
// If the number is greater than zero, prepend the "+" sign
72+
if (num > 0) {
73+
return `+${num}`;
74+
}
75+
// Zero or negative numbers will naturally format with their own sign or nothing
76+
return num.toString();
77+
},
78+
},
79+
{
80+
headerName: 'CNV State',
81+
field: 'cnvState',
82+
},
83+
{
84+
headerName: 'Chr:band',
85+
field: 'chromosomeBand',
86+
},
87+
{
88+
...actionsColDef,
89+
}];
90+
91+
const structuralVariantsColumnDefs: ColDef[] = [{
92+
headerName: 'Genes 5`::3`',
93+
colId: 'genes',
94+
cellRenderer: 'GeneCellRenderer',
95+
cellRendererParams: { link: true },
96+
valueGetter: createGeneRelatedValueGetter('name', ' :: '),
97+
},
98+
{
99+
headerName: 'Exons 5`/3`',
100+
colId: 'exons',
101+
valueGetter: (params) => (params.data.exon1 && params.data.exon2
102+
? `${params.data.exon1}:${params.data.exon2}`
103+
: (params.data.exon1 || params.data.exon2)),
104+
},
105+
{
106+
headerName: 'Breakpoint',
107+
colId: 'breakpoint',
108+
field: 'breakpoint',
109+
},
110+
{
111+
headerName: 'Event Type',
112+
colId: 'eventType',
113+
field: 'eventType',
114+
},
115+
{
116+
headerName: 'Sample',
117+
colId: 'detectedIn',
118+
field: 'detectedIn',
119+
},
120+
{
121+
headerName: 'Cytogenic Description',
122+
colId: 'conventionalName',
123+
field: 'conventionalName',
124+
},
125+
{
126+
...actionsColDef,
127+
}];
128+
129+
const expressionColumnDefs: Array<ColDef | ColGroupDef> = [{
130+
headerName: 'Gene',
131+
field: 'gene.name',
132+
cellRenderer: 'GeneCellRenderer',
133+
cellRendererParams: { link: true },
134+
},
135+
{
136+
headerName: 'Expression Class',
137+
field: 'expressionState',
138+
},
139+
{
140+
headerName: 'Disease',
141+
children: [
142+
{ headerName: 'Perc', field: 'diseasePercentile' },
143+
{ headerName: 'Z-Score', field: 'diseaseZScore' },
144+
],
145+
},
146+
{
147+
...actionsColDef,
148+
}];
149+
150+
export {
151+
smallMutationsColumnDefs,
152+
copyNumberColumnDefs,
153+
expressionColumnDefs,
154+
structuralVariantsColumnDefs,
155+
};

0 commit comments

Comments
 (0)