Skip to content

Commit

Permalink
Merge pull request #554 from bcgsc/release/v6.31.0
Browse files Browse the repository at this point in the history
Release/v6.31.0
  • Loading branch information
kttkjl authored Aug 12, 2024
2 parents a325837 + 5194ecb commit 5f4315b
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 124 deletions.
65 changes: 60 additions & 5 deletions app/components/PrintTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type PrintTableProps = {
collapseableCols?: string[];
noRowsText?: string;
fullWidth?: boolean;
outerRowOrderByInternalCol?: string[];
innerRowOrderByInternalCol?: string[];
};

/**
Expand All @@ -33,6 +35,8 @@ function PrintTable({
noRowsText = '',
collapseableCols = null,
fullWidth = false,
outerRowOrderByInternalCol = null,
innerRowOrderByInternalCol = null,
}: PrintTableProps): JSX.Element {
const sortedColDefs = useMemo(() => columnDefs
.filter((col) => (col.headerName && col.hide !== true && col.headerName !== 'Actions'))
Expand Down Expand Up @@ -84,12 +88,63 @@ function PrintTable({
const colIdxsToCombine = {};
const rowIdxsToSkip = {};
const rowIdxsToExpand = {};
let outerRowsRank = {};

if (outerRowOrderByInternalCol) {
outerRowsRank = data.reduce((acc, row) => {
const rowkey = JSON.stringify(collapseableCols.map((val) => row[val]));
if (!acc[rowkey]) {
acc[rowkey] = {};
}
Object.keys(row).forEach((key) => {
if (outerRowOrderByInternalCol.includes(key)) {
if (!acc[rowkey][key]) {
acc[rowkey][key] = [];
}
acc[rowkey][key].push(row[key]);
}
});
return acc;
}, {});

Object.keys(outerRowsRank).forEach((key) => {
Object.keys(outerRowsRank[key]).forEach((subkey) => {
outerRowsRank[key][subkey] = JSON.stringify(outerRowsRank[key][subkey].sort()); // Sort and stringify
});
});
}

(collapseableCols?.length > 0 ? [...data].sort((aRow, bRow) => {
const aKey = collapseableCols.map((val) => aRow[val]);
const bKey = collapseableCols.map((val) => bRow[val]);
if (aKey === bKey) return 0;
return aKey > bKey ? 1 : -1;
const aKey = JSON.stringify(collapseableCols.map((val) => aRow[val]));
const bKey = JSON.stringify(collapseableCols.map((val) => bRow[val]));
// ordering inner rows (rows with matching aKey/bKey)
if (aKey === bKey) {
// order by 'innerRowOrderByInternalCol' index 0 if possible, then index 1, etc
if (innerRowOrderByInternalCol) {
for (let i = 0; i < innerRowOrderByInternalCol.length; i++) {
const col = innerRowOrderByInternalCol[i];
// if the current column values are equal, move to next column
// otherwise return rank based on these columns
if (!(aRow[col] === bRow[col])) {
return aRow[col] > bRow[col] ? 1 : -1;
}
}
}
return 0;
}
// ordering outer rows (rows with different aKey/bKey)
if (outerRowOrderByInternalCol) {
// order by 'outerRowOrderByInternalCol' if possible, then by outer row key if necessary
for (let i = 0; i < outerRowOrderByInternalCol.length; i++) {
const rankA = outerRowsRank[aKey][outerRowOrderByInternalCol[i]];
const rankB = outerRowsRank[bKey][outerRowOrderByInternalCol[i]];
if (!(rankA === rankB)) {
return rankA > rankB ? 1 : -1;
}
}
return aKey > bKey ? 1 : -1;
}
return 0;
}) : data).forEach((dataRow, rowIdx) => {
const rowData = [];
currRowKey = '';
Expand Down Expand Up @@ -178,7 +233,7 @@ function PrintTable({
}
}
return component;
}, [sortedColDefs, noRowsText, data, collapseableCols]);
}, [sortedColDefs, noRowsText, data, collapseableCols, outerRowOrderByInternalCol, innerRowOrderByInternalCol]);

const tableId = useMemo(() => {
if (data?.length) {
Expand Down
37 changes: 24 additions & 13 deletions app/components/SignatureCard/__tests__/SignatureCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import {
render, screen, fireEvent, waitFor,
} from '@testing-library/react';

import ReportContext from '@/context/ReportContext';
import SignatureCard, { SignatureType } from '..';
import { mockNullData, mockNullObjectData, mockObjectData } from './mockData';
import {
mockNullData, mockNullObjectData, mockObjectData, mockReportData,
} from './mockData';

jest.mock('@/services/SnackbarUtils');
jest.mock('@/services/api');

describe('SignatureCard', () => {
test('Author and sign button are visible', async () => {
render(
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
<SignatureCard
title="Author"
type="author"
Expand All @@ -23,7 +30,7 @@ describe('SignatureCard', () => {

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

test('Sign button calls onClick', async () => {
const handleClick = jest.fn();
const handleSign = jest.fn();
render(
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
<SignatureCard
title="Author"
type="author"
signatures={null}
onClick={handleClick}
onClick={handleSign}
/>
</ReportContext.Provider>,
);
fireEvent.click(await screen.findByRole('button', { name: 'Sign' }));

expect(handleClick).toHaveBeenCalledTimes(1);
waitFor(() => {
expect(handleSign).toHaveBeenCalledTimes(1);
});
});

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

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

test('Reviewer name & remove signature button are visible', async () => {
render(
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
<SignatureCard
title="Reviewer"
type="reviewer"
Expand All @@ -94,13 +103,15 @@ describe('SignatureCard', () => {
</ReportContext.Provider>,
);

expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
waitFor(() => {
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
});
expect(await screen.findByRole('button', { name: /^((?!Sign).)*$/ })).toBeInTheDocument();
});

test('No buttons are visible in print view', async () => {
render(
<ReportContext.Provider value={{ canEdit: true, report: null, setReport: () => {} }}>
<ReportContext.Provider value={{ canEdit: true, report: mockReportData, setReport: () => {} }}>
<SignatureCard
title="Reviewer"
type="reviewer"
Expand Down
31 changes: 31 additions & 0 deletions app/components/SignatureCard/__tests__/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,39 @@ const mockObjectData = {
reviewerSignedAt: '2020',
};

const mockReportData = {
ident: 'mockIdent',
createdAt: '2020',
updatedAt: '2020',
alternateIdentifier: null,
pediatricIds: null,
analysisStartedAt: null,
biopsyName: null,
createdBy: null,
expression_matrix: null,
kbVersion: null,
patientId: null,
patientInformation: null,
ploidy: null,
projects: null,
reportVersion: null,
sampleInfo: null,
state: null,
subtyping: null,
template: null,
tumourContent: null,
type: null,
users: null,
oncotreeTumourType: null,
kbDiseaseMatch: null,
m1m2Score: null,
captiv8Score: null,
appendix: null,
};

export {
mockNullData,
mockNullObjectData,
mockObjectData,
mockReportData,
};
73 changes: 62 additions & 11 deletions app/components/SignatureCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React, {
useState, useEffect, useMemo,
useCallback,
} from 'react';
import {
Paper,
Typography,
IconButton,
Button,
} from '@mui/material';
import api from '@/services/api';
import GestureIcon from '@mui/icons-material/Gesture';
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';

import snackbar from '@/services/SnackbarUtils';
import { UserType } from '@/common';
import useReport from '@/hooks/useReport';
import useResource from '@/hooks/useResource';
import useSecurity from '@/hooks/useSecurity';
import { formatDate } from '@/utils/date';
import { SignatureType, SignatureUserType } from './types';

Expand All @@ -24,7 +28,7 @@ const NON_BREAKING_SPACE = '\u00A0';
export type SignatureCardProps = {
title: string;
signatures: SignatureType;
onClick: (isSigned: boolean, type: string) => void;
onClick: (isSigned: boolean, updatedSignature: SignatureType) => void;
type: SignatureUserType;
isPrint?: boolean;
};
Expand All @@ -36,28 +40,75 @@ const SignatureCard = ({
type,
isPrint = false,
}: SignatureCardProps): JSX.Element => {
const { canEdit } = useReport();
const { reportAssignmentAccess: canAddSignatures } = useResource();
const { canEdit, report, setReport } = useReport();
const { userDetails } = useSecurity();

const [userSignature, setUserSignature] = useState<UserType>();
const [role, setRole] = useState('');

useEffect(() => {
if (signatures && type) {
if (type === 'author') {
setUserSignature(signatures.authorSignature);
setRole('author');
} else if (type === 'reviewer') {
setUserSignature(signatures.reviewerSignature);
setRole('reviewer');
} else if (type === 'creator') {
setUserSignature(signatures.creatorSignature);
setRole('bioinformatician');
}
}
}, [signatures, type, setRole]);

const handleSign = useCallback(async () => {
let newReport = null;

// Assign user
try {
newReport = await api.post(
`/reports/${report.ident}/user`,
// Hardcode analyst role here because report does not accept 'author'
{ user: userDetails.ident, role: 'analyst' },
{},
).request();
} catch (e) {
// If user already assigned, silent fail this add
if (e.content?.status !== 409) {
snackbar.error('Error assigning user to report: ', e.message);
}
}
}, [signatures, type]);

const handleSign = () => {
onClick(true, type);
};
// Do signature
try {
const newSignature = await api.put(
`/reports/${report.ident}/signatures/sign/${role}`,
{},
).request();

const handleRevoke = () => {
onClick(false, type);
};
if (newReport) {
setReport(newReport);
}
onClick(true, newSignature);
snackbar.success('User assigned to report');
} catch (err) {
snackbar.error(`Error adding user: ${err}`);
}
}, [onClick, report.ident, role, setReport, userDetails.ident]);

const handleRevoke = useCallback(async () => {
try {
const newSignature = await api.put(
`/reports/${report.ident}/signatures/revoke/${role}`,
{},
).request();
onClick(false, newSignature);
snackbar.success('User removed from report');
} catch (err) {
snackbar.error(`Error removing user: ${err}`);
}
}, [onClick, report.ident, role]);

const renderDate = useMemo(() => {
if (signatures?.ident) {
Expand Down Expand Up @@ -129,7 +180,7 @@ const SignatureCard = ({
{userSignature.lastName}
</Typography>
)}
{!userSignature?.ident && canEdit && (
{!userSignature?.ident && (canEdit || canAddSignatures) && (
<Button
onClick={handleSign}
variant="text"
Expand Down
12 changes: 12 additions & 0 deletions app/context/ResourceContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,27 @@ const REPORTS_BLOCK = [];
const ADMIN_ACCESS = ['admin'];
const ADMIN_BLOCK = [...ALL_ROLES, ...NO_GROUP_MATCH];

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

const [germlineAccess, setGermlineAccess] = useState(false);
const [reportsAccess, setReportsAccess] = useState(false);
/**
* Is the user allowed to edit the report
*/
const [reportEditAccess, setReportEditAccess] = useState(false);
/**
* Is the user allowed to assign users to the report
*/
const [reportAssignmentAccess, setReportAssignmentAccess] = useState(false);
const [adminAccess, setAdminAccess] = useState(false);
const [managerAccess, setManagerAccess] = useState(false);
/**
* Is the user allowed to see the settings page
*/
const [reportSettingAccess, setReportSettingAccess] = useState(false);
const [unreviewedAccess, setUnreviewedAccess] = useState(false);
const [nonproductionAccess, setNonproductionAccess] = useState(false);
Expand Down
Loading

0 comments on commit 5f4315b

Please sign in to comment.