Skip to content

Commit 5f4315b

Browse files
authored
Merge pull request #554 from bcgsc/release/v6.31.0
Release/v6.31.0
2 parents a325837 + 5194ecb commit 5f4315b

File tree

18 files changed

+249
-124
lines changed

18 files changed

+249
-124
lines changed

app/components/PrintTable/index.tsx

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export type PrintTableProps = {
2020
collapseableCols?: string[];
2121
noRowsText?: string;
2222
fullWidth?: boolean;
23+
outerRowOrderByInternalCol?: string[];
24+
innerRowOrderByInternalCol?: string[];
2325
};
2426

2527
/**
@@ -33,6 +35,8 @@ function PrintTable({
3335
noRowsText = '',
3436
collapseableCols = null,
3537
fullWidth = false,
38+
outerRowOrderByInternalCol = null,
39+
innerRowOrderByInternalCol = null,
3640
}: PrintTableProps): JSX.Element {
3741
const sortedColDefs = useMemo(() => columnDefs
3842
.filter((col) => (col.headerName && col.hide !== true && col.headerName !== 'Actions'))
@@ -84,12 +88,63 @@ function PrintTable({
8488
const colIdxsToCombine = {};
8589
const rowIdxsToSkip = {};
8690
const rowIdxsToExpand = {};
91+
let outerRowsRank = {};
92+
93+
if (outerRowOrderByInternalCol) {
94+
outerRowsRank = data.reduce((acc, row) => {
95+
const rowkey = JSON.stringify(collapseableCols.map((val) => row[val]));
96+
if (!acc[rowkey]) {
97+
acc[rowkey] = {};
98+
}
99+
Object.keys(row).forEach((key) => {
100+
if (outerRowOrderByInternalCol.includes(key)) {
101+
if (!acc[rowkey][key]) {
102+
acc[rowkey][key] = [];
103+
}
104+
acc[rowkey][key].push(row[key]);
105+
}
106+
});
107+
return acc;
108+
}, {});
109+
110+
Object.keys(outerRowsRank).forEach((key) => {
111+
Object.keys(outerRowsRank[key]).forEach((subkey) => {
112+
outerRowsRank[key][subkey] = JSON.stringify(outerRowsRank[key][subkey].sort()); // Sort and stringify
113+
});
114+
});
115+
}
87116

88117
(collapseableCols?.length > 0 ? [...data].sort((aRow, bRow) => {
89-
const aKey = collapseableCols.map((val) => aRow[val]);
90-
const bKey = collapseableCols.map((val) => bRow[val]);
91-
if (aKey === bKey) return 0;
92-
return aKey > bKey ? 1 : -1;
118+
const aKey = JSON.stringify(collapseableCols.map((val) => aRow[val]));
119+
const bKey = JSON.stringify(collapseableCols.map((val) => bRow[val]));
120+
// ordering inner rows (rows with matching aKey/bKey)
121+
if (aKey === bKey) {
122+
// order by 'innerRowOrderByInternalCol' index 0 if possible, then index 1, etc
123+
if (innerRowOrderByInternalCol) {
124+
for (let i = 0; i < innerRowOrderByInternalCol.length; i++) {
125+
const col = innerRowOrderByInternalCol[i];
126+
// if the current column values are equal, move to next column
127+
// otherwise return rank based on these columns
128+
if (!(aRow[col] === bRow[col])) {
129+
return aRow[col] > bRow[col] ? 1 : -1;
130+
}
131+
}
132+
}
133+
return 0;
134+
}
135+
// ordering outer rows (rows with different aKey/bKey)
136+
if (outerRowOrderByInternalCol) {
137+
// order by 'outerRowOrderByInternalCol' if possible, then by outer row key if necessary
138+
for (let i = 0; i < outerRowOrderByInternalCol.length; i++) {
139+
const rankA = outerRowsRank[aKey][outerRowOrderByInternalCol[i]];
140+
const rankB = outerRowsRank[bKey][outerRowOrderByInternalCol[i]];
141+
if (!(rankA === rankB)) {
142+
return rankA > rankB ? 1 : -1;
143+
}
144+
}
145+
return aKey > bKey ? 1 : -1;
146+
}
147+
return 0;
93148
}) : data).forEach((dataRow, rowIdx) => {
94149
const rowData = [];
95150
currRowKey = '';
@@ -178,7 +233,7 @@ function PrintTable({
178233
}
179234
}
180235
return component;
181-
}, [sortedColDefs, noRowsText, data, collapseableCols]);
236+
}, [sortedColDefs, noRowsText, data, collapseableCols, outerRowOrderByInternalCol, innerRowOrderByInternalCol]);
182237

183238
const tableId = useMemo(() => {
184239
if (data?.length) {

app/components/SignatureCard/__tests__/SignatureCard.test.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import React from 'react';
2-
import { render, screen, fireEvent } from '@testing-library/react';
2+
import {
3+
render, screen, fireEvent, waitFor,
4+
} from '@testing-library/react';
35

46
import ReportContext from '@/context/ReportContext';
57
import SignatureCard, { SignatureType } from '..';
6-
import { mockNullData, mockNullObjectData, mockObjectData } from './mockData';
8+
import {
9+
mockNullData, mockNullObjectData, mockObjectData, mockReportData,
10+
} from './mockData';
11+
12+
jest.mock('@/services/SnackbarUtils');
13+
jest.mock('@/services/api');
714

815
describe('SignatureCard', () => {
916
test('Author and sign button are visible', async () => {
1017
render(
11-
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
18+
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
1219
<SignatureCard
1320
title="Author"
1421
type="author"
@@ -23,7 +30,7 @@ describe('SignatureCard', () => {
2330

2431
test('Sign button is not visible without edit permissions', async () => {
2532
render(
26-
<ReportContext.Provider value={{ canEdit: false, report: null, setReport: () => {} }}>
33+
<ReportContext.Provider value={{ canEdit: false, report: mockReportData, setReport: () => {} }}>
2734
<SignatureCard
2835
title="Author"
2936
type="author"
@@ -36,25 +43,27 @@ describe('SignatureCard', () => {
3643
});
3744

3845
test('Sign button calls onClick', async () => {
39-
const handleClick = jest.fn();
46+
const handleSign = jest.fn();
4047
render(
41-
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
48+
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
4249
<SignatureCard
4350
title="Author"
4451
type="author"
4552
signatures={null}
46-
onClick={handleClick}
53+
onClick={handleSign}
4754
/>
4855
</ReportContext.Provider>,
4956
);
5057
fireEvent.click(await screen.findByRole('button', { name: 'Sign' }));
5158

52-
expect(handleClick).toHaveBeenCalledTimes(1);
59+
waitFor(() => {
60+
expect(handleSign).toHaveBeenCalledTimes(1);
61+
});
5362
});
5463

5564
test('Sign button is visible when reviewerSignature is null', async () => {
5665
render(
57-
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
66+
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
5867
<SignatureCard
5968
title="Reviewer"
6069
type="reviewer"
@@ -69,7 +78,7 @@ describe('SignatureCard', () => {
6978

7079
test('Sign button is visible when reviewerSignature has null data', async () => {
7180
render(
72-
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
81+
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
7382
<SignatureCard
7483
title="Reviewer"
7584
type="reviewer"
@@ -84,7 +93,7 @@ describe('SignatureCard', () => {
8493

8594
test('Reviewer name & remove signature button are visible', async () => {
8695
render(
87-
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
96+
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
8897
<SignatureCard
8998
title="Reviewer"
9099
type="reviewer"
@@ -94,13 +103,15 @@ describe('SignatureCard', () => {
94103
</ReportContext.Provider>,
95104
);
96105

97-
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
106+
waitFor(() => {
107+
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
108+
});
98109
expect(await screen.findByRole('button', { name: /^((?!Sign).)*$/ })).toBeInTheDocument();
99110
});
100111

101112
test('No buttons are visible in print view', async () => {
102113
render(
103-
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
114+
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
104115
<SignatureCard
105116
title="Reviewer"
106117
type="reviewer"

app/components/SignatureCard/__tests__/mockData.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,39 @@ const mockObjectData = {
4848
reviewerSignedAt: '2020',
4949
};
5050

51+
const mockReportData = {
52+
ident: 'mockIdent',
53+
createdAt: '2020',
54+
updatedAt: '2020',
55+
alternateIdentifier: null,
56+
pediatricIds: null,
57+
analysisStartedAt: null,
58+
biopsyName: null,
59+
createdBy: null,
60+
expression_matrix: null,
61+
kbVersion: null,
62+
patientId: null,
63+
patientInformation: null,
64+
ploidy: null,
65+
projects: null,
66+
reportVersion: null,
67+
sampleInfo: null,
68+
state: null,
69+
subtyping: null,
70+
template: null,
71+
tumourContent: null,
72+
type: null,
73+
users: null,
74+
oncotreeTumourType: null,
75+
kbDiseaseMatch: null,
76+
m1m2Score: null,
77+
captiv8Score: null,
78+
appendix: null,
79+
};
80+
5181
export {
5282
mockNullData,
5383
mockNullObjectData,
5484
mockObjectData,
85+
mockReportData,
5586
};

app/components/SignatureCard/index.tsx

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import React, {
22
useState, useEffect, useMemo,
3+
useCallback,
34
} from 'react';
45
import {
56
Paper,
67
Typography,
78
IconButton,
89
Button,
910
} from '@mui/material';
11+
import api from '@/services/api';
1012
import GestureIcon from '@mui/icons-material/Gesture';
1113
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';
12-
14+
import snackbar from '@/services/SnackbarUtils';
1315
import { UserType } from '@/common';
1416
import useReport from '@/hooks/useReport';
17+
import useResource from '@/hooks/useResource';
18+
import useSecurity from '@/hooks/useSecurity';
1519
import { formatDate } from '@/utils/date';
1620
import { SignatureType, SignatureUserType } from './types';
1721

@@ -24,7 +28,7 @@ const NON_BREAKING_SPACE = '\u00A0';
2428
export type SignatureCardProps = {
2529
title: string;
2630
signatures: SignatureType;
27-
onClick: (isSigned: boolean, type: string) => void;
31+
onClick: (isSigned: boolean, updatedSignature: SignatureType) => void;
2832
type: SignatureUserType;
2933
isPrint?: boolean;
3034
};
@@ -36,28 +40,75 @@ const SignatureCard = ({
3640
type,
3741
isPrint = false,
3842
}: SignatureCardProps): JSX.Element => {
39-
const { canEdit } = useReport();
43+
const { reportAssignmentAccess: canAddSignatures } = useResource();
44+
const { canEdit, report, setReport } = useReport();
45+
const { userDetails } = useSecurity();
46+
4047
const [userSignature, setUserSignature] = useState<UserType>();
48+
const [role, setRole] = useState('');
4149

4250
useEffect(() => {
4351
if (signatures && type) {
4452
if (type === 'author') {
4553
setUserSignature(signatures.authorSignature);
54+
setRole('author');
4655
} else if (type === 'reviewer') {
4756
setUserSignature(signatures.reviewerSignature);
57+
setRole('reviewer');
4858
} else if (type === 'creator') {
4959
setUserSignature(signatures.creatorSignature);
60+
setRole('bioinformatician');
61+
}
62+
}
63+
}, [signatures, type, setRole]);
64+
65+
const handleSign = useCallback(async () => {
66+
let newReport = null;
67+
68+
// Assign user
69+
try {
70+
newReport = await api.post(
71+
`/reports/${report.ident}/user`,
72+
// Hardcode analyst role here because report does not accept 'author'
73+
{ user: userDetails.ident, role: 'analyst' },
74+
{},
75+
).request();
76+
} catch (e) {
77+
// If user already assigned, silent fail this add
78+
if (e.content?.status !== 409) {
79+
snackbar.error('Error assigning user to report: ', e.message);
5080
}
5181
}
52-
}, [signatures, type]);
5382

54-
const handleSign = () => {
55-
onClick(true, type);
56-
};
83+
// Do signature
84+
try {
85+
const newSignature = await api.put(
86+
`/reports/${report.ident}/signatures/sign/${role}`,
87+
{},
88+
).request();
5789

58-
const handleRevoke = () => {
59-
onClick(false, type);
60-
};
90+
if (newReport) {
91+
setReport(newReport);
92+
}
93+
onClick(true, newSignature);
94+
snackbar.success('User assigned to report');
95+
} catch (err) {
96+
snackbar.error(`Error adding user: ${err}`);
97+
}
98+
}, [onClick, report.ident, role, setReport, userDetails.ident]);
99+
100+
const handleRevoke = useCallback(async () => {
101+
try {
102+
const newSignature = await api.put(
103+
`/reports/${report.ident}/signatures/revoke/${role}`,
104+
{},
105+
).request();
106+
onClick(false, newSignature);
107+
snackbar.success('User removed from report');
108+
} catch (err) {
109+
snackbar.error(`Error removing user: ${err}`);
110+
}
111+
}, [onClick, report.ident, role]);
61112

62113
const renderDate = useMemo(() => {
63114
if (signatures?.ident) {
@@ -129,7 +180,7 @@ const SignatureCard = ({
129180
{userSignature.lastName}
130181
</Typography>
131182
)}
132-
{!userSignature?.ident && canEdit && (
183+
{!userSignature?.ident && (canEdit || canAddSignatures) && (
133184
<Button
134185
onClick={handleSign}
135186
variant="text"

app/context/ResourceContext/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,27 @@ const REPORTS_BLOCK = [];
2525
const ADMIN_ACCESS = ['admin'];
2626
const ADMIN_BLOCK = [...ALL_ROLES, ...NO_GROUP_MATCH];
2727

28+
/**
29+
* Checks user permissions based on the groups they are assigned, nothing report-specific
30+
*/
2831
const useResources = (): ResourceContextType => {
2932
const { userDetails: { groups } } = useSecurity();
3033

3134
const [germlineAccess, setGermlineAccess] = useState(false);
3235
const [reportsAccess, setReportsAccess] = useState(false);
36+
/**
37+
* Is the user allowed to edit the report
38+
*/
3339
const [reportEditAccess, setReportEditAccess] = useState(false);
40+
/**
41+
* Is the user allowed to assign users to the report
42+
*/
3443
const [reportAssignmentAccess, setReportAssignmentAccess] = useState(false);
3544
const [adminAccess, setAdminAccess] = useState(false);
3645
const [managerAccess, setManagerAccess] = useState(false);
46+
/**
47+
* Is the user allowed to see the settings page
48+
*/
3749
const [reportSettingAccess, setReportSettingAccess] = useState(false);
3850
const [unreviewedAccess, setUnreviewedAccess] = useState(false);
3951
const [nonproductionAccess, setNonproductionAccess] = useState(false);

0 commit comments

Comments
 (0)