diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ClinicalEntityData.tsx b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ClinicalEntityData.tsx
new file mode 100644
index 00000000..5ed012ee
--- /dev/null
+++ b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ClinicalEntityData.tsx
@@ -0,0 +1,291 @@
+/*
+ * Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved
+ *
+ * This program and the accompanying materials are made available under the terms of
+ * the GNU Affero General Public License v3.0. You should have received a copy of the
+ * GNU Affero General Public License along with this program.
+ * If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import { ClinicalInput, ClinicalSearchResults } from '@/__generated__/clinical/graphql';
+import CLINICAL_ENTITY_DATA_QUERY from '@/app/gql/clinical/CLINICAL_ENTITY_DATA_QUERY';
+import { useClinicalQuery } from '@/app/hooks/useApolloQuery';
+import { ContentPlaceholder, DnaLoader, css } from '@icgc-argo/uikit';
+import { useEffect } from 'react';
+import {
+ ClinicalEntitySearchResultResponse,
+ CompletionStates,
+ aliasSortNames,
+ aliasedEntityNames,
+ clinicalEntityFields,
+ defaultClinicalEntityFilters,
+ emptyClinicalDataResponse,
+ emptySearchResponse,
+} from '../common';
+import { formatTableErrors } from '../tableDataRefactor';
+import ClinicalEntityDataTable from './ClinicalEntityDataTable';
+import { ErrorTable, ErrorTableProps } from './ErrorTable';
+import { mock } from './mock';
+import {
+ defaultDonorSettings,
+ defaultEntityPageSettings,
+ usePageSettings,
+} from './usePageSettings';
+
+export type DonorEntry = {
+ row: string;
+ isNew: boolean;
+ [k: string]: string | number | boolean;
+};
+
+const NoDataCell = () => (
+
+
+
+
+
+);
+
+const defaultErrorPageSettings = {
+ page: 0,
+ pageSize: 5,
+ sorted: [{ id: 'donorId', desc: true }],
+};
+
+const validateEntityQueryName = (entityQuery) => {
+ const entities = typeof entityQuery === 'string' ? [entityQuery] : entityQuery;
+ return entities.map((entityName) => clinicalEntityFields.find((entity) => entity === entityName));
+};
+
+type GetEntityDataProps = {
+ program: string;
+ entityType: string | string[];
+ page: number;
+ pageSize: number;
+ sort: string;
+ completionState: CompletionStates;
+ donorIds: number[];
+ submitterDonorIds: string[];
+};
+export const useGetEntityData = ({
+ program,
+ entityType,
+ page,
+ pageSize,
+ sort,
+ completionState,
+ donorIds,
+ submitterDonorIds,
+}: GetEntityDataProps) => {
+ console.log('get data', {
+ program,
+ entityType,
+ page,
+ pageSize,
+ sort,
+ completionState,
+ donorIds,
+ submitterDonorIds,
+ });
+ const entityTypes = validateEntityQueryName(entityType);
+
+ return useClinicalQuery(CLINICAL_ENTITY_DATA_QUERY, {
+ errorPolicy: 'all',
+ fetchPolicy: 'cache-and-network',
+ variables: {
+ programShortName: program,
+ filters: {
+ ...defaultClinicalEntityFilters,
+ sort,
+ page,
+ pageSize,
+ completionState,
+ donorIds,
+ submitterDonorIds,
+ entityTypes,
+ },
+ },
+ });
+};
+
+/**
+ *
+ * returns a donorId array and a submitterDonorId array to filter
+ */
+type CI = Required>;
+const getDonorIdFilters = ({ searchResults, currentDonors, useDefaultQuery }): CI => {
+ const donorIds = useDefaultQuery
+ ? []
+ : currentDonors.length
+ ? currentDonors
+ : searchResults.map(({ donorId }: ClinicalSearchResults) => donorId);
+
+ const submitterDonorIds =
+ useDefaultQuery || currentDonors.length
+ ? []
+ : searchResults
+ .map(({ submitterDonorId }: ClinicalSearchResults) => submitterDonorId)
+ .filter((id) => !!id);
+
+ return {
+ donorIds,
+ submitterDonorIds,
+ };
+};
+
+// query, sort, format data for table, search etc
+type ClinicalEntityDataTableProps = {
+ entityType: string;
+ program: string;
+ completionState: CompletionStates;
+ currentDonors: number[];
+ donorSearchResults: ClinicalEntitySearchResultResponse;
+ useDefaultQuery: boolean;
+ noData: boolean;
+};
+const ClinicalEntityData = ({
+ entityType,
+ program,
+ completionState = CompletionStates['all'],
+ currentDonors,
+ donorSearchResults = emptySearchResponse,
+ useDefaultQuery,
+ noData,
+}: ClinicalEntityDataTableProps) => {
+ // Clinical Data table page
+ const defaultPageSettings =
+ useDefaultQuery && entityType === 'donor' ? defaultDonorSettings : defaultEntityPageSettings;
+ const [pageSettings, setPageSettings] = usePageSettings(defaultPageSettings);
+ const { page, pageSize, sorted } = pageSettings;
+
+ // reset paging
+ useEffect(() => {
+ setPageSettings(defaultPageSettings);
+ }, [entityType, useDefaultQuery]);
+
+ const { desc, id } = sorted[0];
+ const sortKey = aliasSortNames[id] || id;
+ const sort = `${desc ? '-' : ''}${sortKey}`;
+
+ const {
+ clinicalSearchResults: { searchResults, totalResults },
+ } = donorSearchResults || emptySearchResponse;
+ const { donorIds, submitterDonorIds: submittedDonorIdsToFilter } = getDonorIdFilters({
+ currentDonors,
+ searchResults,
+ useDefaultQuery,
+ });
+ const nextSearchPage = (page + 1) * pageSize;
+ const clinicalDataPropertyFilters = {
+ donorIds,
+ submitterDonorIds: submittedDonorIdsToFilter.slice(
+ page * pageSize,
+ nextSearchPage < totalResults ? nextSearchPage : totalResults,
+ ),
+ };
+
+ const updatePageSettings = (key, value) => {
+ const newPageSettings = { ...pageSettings, [key]: value };
+
+ if (key === 'pageSize' && value !== pageSettings.pageSize) {
+ // Prevents bug querying nonexistent data
+ newPageSettings.page = 0;
+ }
+ setPageSettings(newPageSettings);
+ return newPageSettings;
+ };
+
+ // API query
+ const { data: clinicalEntityData, loading } = useGetEntityData({
+ program,
+ entityType,
+ completionState,
+ page,
+ pageSize,
+ sort,
+ ...clinicalDataPropertyFilters,
+ });
+
+ // This is the core of the logic here
+ // this data response => UI
+ console.log('data', clinicalEntityData);
+
+ const { clinicalData } =
+ clinicalEntityData == undefined || loading ? emptyClinicalDataResponse : clinicalEntityData;
+
+ const noTableData = noData || clinicalData.clinicalEntities.length === 0;
+
+ const aliasedEntityName = aliasedEntityNames[entityType];
+
+ const { clinicalErrors = [] } = clinicalData;
+ const { tableErrors, totalErrorsAmount } = formatTableErrors({
+ clinicalErrors: mock.clinicalErrors,
+ aliasedEntityName: mock.aliasedEntityName,
+ });
+ console.log('te', tableErrors, 'total', totalErrorsAmount);
+
+ const errorTableProps: ErrorTableProps = {
+ tableErrors,
+ totalErrorsAmount,
+ entityType,
+ program,
+ };
+
+ const clinicalDataTableProps = {
+ aliasedEntityName,
+ totalResults,
+ page,
+ pageSize,
+ entityType,
+ currentDonors,
+ useDefaultQuery,
+ clinicalData,
+ sortingFn: () => null,
+ };
+
+ return loading ? (
+
+ ) : noTableData ? (
+
+ ) : (
+ <>
+ {totalErrorsAmount > 0 && (
+
+
+
+ )}
+
+ >
+ );
+};
+
+export default ClinicalEntityData;
diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ClinicalEntityDataTable.tsx b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ClinicalEntityDataTable.tsx
new file mode 100644
index 00000000..915facb0
--- /dev/null
+++ b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ClinicalEntityDataTable.tsx
@@ -0,0 +1,435 @@
+/*
+ * Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved
+ *
+ * This program and the accompanying materials are made available under the terms of
+ * the GNU Affero General Public License v3.0. You should have received a copy of the
+ * GNU Affero General Public License along with this program.
+ * If not, see .
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+import { TableInfoHeaderContainer } from '@/app/components/Table/common';
+import { Icon, Table, Typography, css, useTheme } from '@icgc-argo/uikit';
+import memoize from 'lodash/memoize';
+import { createRef, useState } from 'react';
+import {
+ Cell,
+ ClinicalCoreCompletionHeader,
+ TopLevelHeader,
+ styleThickBorder,
+} from '../ClinicalDataTableComp';
+import { aliasedEntityFields, aliasedEntityNames } from '../common';
+
+export type DonorEntry = {
+ row: string;
+ isNew: boolean;
+ [k: string]: string | number | boolean;
+};
+
+const emptyCompletion = {
+ DO: 0,
+ PD: 0,
+ FO: 0,
+ NS: 0,
+ TR: 0,
+ TS: 0,
+};
+
+const completionColumnHeaders = {
+ donor: 'DO',
+ primaryDiagnosis: 'PD',
+ normalSpecimens: 'NS',
+ tumourSpecimens: 'TS',
+ treatments: 'TR',
+ followUps: 'FO',
+};
+
+const coreCompletionFields = Object.keys(completionColumnHeaders);
+
+const getColumnWidth = memoize<(keyString: string, showCompletionStats: boolean) => number>(
+ (keyString, showCompletionStats) => {
+ const minWidth = keyString === 'donor_id' ? 70 : showCompletionStats ? 40 : 95;
+ const maxWidth = showCompletionStats ? 45 : 200;
+ const spacePerChar = 8;
+ const margin = 10;
+ const targetWidth = keyString.length * spacePerChar + margin;
+ return Math.max(Math.min(maxWidth, targetWidth), minWidth);
+ },
+);
+
+const parseRecords = (records, showCompletionStats, completionStats) =>
+ records.map((record) => {
+ let clinicalRecord = {};
+ record.forEach((r) => {
+ const displayKey = r.name;
+ clinicalRecord[displayKey] = displayKey === 'donor_id' ? `DO${r.value}` : r.value || '';
+ if (showCompletionStats && displayKey === 'donor_id') {
+ const completionRecord = completionStats.find((stat) => stat.donorId === parseInt(r.value));
+
+ if (!completionRecord) {
+ clinicalRecord = { ...clinicalRecord, ...emptyCompletion };
+ } else {
+ const { coreCompletion, entityData: completionEntityData } = completionRecord;
+
+ coreCompletionFields.forEach((field) => {
+ const completionField = completionColumnHeaders[field];
+ const isSpecimenField =
+ completionField === completionColumnHeaders['normalSpecimens'] ||
+ completionField === completionColumnHeaders['tumourSpecimens'];
+
+ if (!isSpecimenField) {
+ const completionValue = coreCompletion[field];
+ clinicalRecord[completionField] = completionValue || 0;
+ } else {
+ const {
+ specimens: {
+ coreCompletionPercentage,
+ normalSpecimensPercentage,
+ tumourSpecimensPercentage,
+ normalSubmissions,
+ tumourSubmissions,
+ },
+ } = completionEntityData;
+
+ if (coreCompletionPercentage === 1) {
+ clinicalRecord[completionField] = 1;
+ } else {
+ const currentPercentage =
+ completionField === completionColumnHeaders['normalSpecimens']
+ ? normalSpecimensPercentage
+ : tumourSpecimensPercentage;
+ const currentSubmissions =
+ completionField === completionColumnHeaders['normalSpecimens']
+ ? normalSubmissions
+ : tumourSubmissions;
+ const hasErrors = currentPercentage !== 1;
+ const value = hasErrors ? currentSubmissions : currentPercentage;
+ clinicalRecord[completionField] = value;
+ }
+ }
+ });
+ }
+ }
+ });
+
+ return clinicalRecord;
+ });
+
+// very hardcoded styling logic
+const getHeaderBorder = (key, showCompletionStats) =>
+ (showCompletionStats && key === completionColumnHeaders.followUps) ||
+ (!showCompletionStats && key === 'donor_id') ||
+ key === 'FO'
+ ? styleThickBorder
+ : '';
+
+const getCellStyles = (
+ state,
+ row,
+ column,
+ showCompletionStats,
+ clinicalErrors,
+ entityType,
+ clinicalData,
+ theme,
+ stickyDonorIDColumnsWidth,
+) => {
+ const { original } = row;
+ const { id } = column;
+ const isCompletionCell =
+ showCompletionStats && Object.values(completionColumnHeaders).includes(id);
+
+ const isSpecimenCell =
+ isCompletionCell &&
+ (id === completionColumnHeaders.normalSpecimens ||
+ id === completionColumnHeaders.tumourSpecimens);
+
+ const originalDonorId = original['donor_id'];
+ const cellDonorId = parseInt(
+ originalDonorId && originalDonorId.includes('DO')
+ ? originalDonorId.substring(2)
+ : originalDonorId,
+ );
+
+ const donorErrorData = clinicalErrors
+ .filter((donor) => donor.donorId === cellDonorId)
+ .map((donor) => donor.errors)
+ .flat();
+
+ const columnErrorData =
+ donorErrorData.length &&
+ donorErrorData.filter(
+ (error) =>
+ error &&
+ (error.entityName === entityType ||
+ (aliasedEntityFields.includes(error.entityName) &&
+ aliasedEntityNames[entityType] === error.entityName)) &&
+ error.fieldName === id,
+ );
+
+ const hasClinicalErrors = columnErrorData && columnErrorData.length >= 1;
+
+ let hasCompletionErrors = isCompletionCell && original[id] !== 1;
+
+ if (isSpecimenCell) {
+ const completionData = clinicalData.clinicalEntities.find(
+ (entity) => entity.entityName === aliasedEntityNames['donor'],
+ ).completionStats;
+
+ const completionRecord =
+ isCompletionCell &&
+ completionData.find((stat) => stat.donorId === parseInt(originalDonorId.substr(2)));
+
+ if (completionRecord) {
+ const { entityData: completionEntityData } = completionRecord;
+
+ const {
+ specimens: { normalSpecimensPercentage, tumourSpecimensPercentage },
+ } = completionEntityData;
+
+ const currentPercentage =
+ id === completionColumnHeaders['normalSpecimens']
+ ? normalSpecimensPercentage
+ : tumourSpecimensPercentage;
+
+ hasCompletionErrors = currentPercentage !== 1;
+ }
+ }
+
+ const specificErrorValue =
+ hasClinicalErrors &&
+ columnErrorData.filter(
+ (error) =>
+ (error.errorType === 'INVALID_BY_SCRIPT' || error.errorType === 'INVALID_ENUM_VALUE') &&
+ (error.info?.value === original[id] ||
+ (error.info?.value && error.info.value[0] === original[id]) ||
+ (error.info.value === null && !Boolean(original[id]))),
+ );
+
+ const fieldError =
+ hasClinicalErrors &&
+ columnErrorData.filter(
+ (error) =>
+ (error.errorType === 'UNRECOGNIZED_FIELD' ||
+ error.errorType === 'MISSING_REQUIRED_FIELD') &&
+ error.fieldName === id,
+ );
+
+ const errorState =
+ // Completion Stats === 1 indicates Complete
+ // 0 is Incomplete, <1 Incorrect Sample / Specimen Ratio
+ (isCompletionCell && hasCompletionErrors) ||
+ specificErrorValue?.length > 0 ||
+ fieldError?.length > 0;
+
+ // use Emotion styling
+ const headerDonorIdStyle = css`
+ background: white,
+ position: absolute,
+ `;
+ const stickyMarginStyle = css`
+ margin-left: ${stickyDonorIDColumnsWidth};
+ `;
+ const style = css`
+ color: ${isCompletionCell && !errorState && theme.colors.accent1_dark};
+ background: ${errorState && theme.colors.error_4};
+ ${getHeaderBorder(id, showCompletionStats)}
+ ${column.Header === 'donor_id' && headerDonorIdStyle};
+ ${column.Header === 'DO' && stickyMarginStyle};
+ ${column.Header === 'program_id' && !showCompletionStats && stickyMarginStyle};
+ `;
+
+ return {
+ style,
+ isCompletionCell,
+ errorState,
+ };
+};
+
+type ClinicalEntityDataTableProps = {
+ entityType: string;
+ currentDonors: number[];
+ useDefaultQuery: boolean;
+ aliasedEntityName: string;
+ page: number;
+ pageSize: number;
+ clinicalData: any;
+ sortingFn: any;
+ totalResults: number;
+};
+const ClinicalEntityDataTable = ({
+ entityType,
+ aliasedEntityName,
+ currentDonors,
+ useDefaultQuery,
+ totalResults,
+ page,
+ pageSize,
+ clinicalData,
+ sortingFn,
+}: ClinicalEntityDataTableProps) => {
+ console.log('cc', clinicalData);
+ const theme = useTheme();
+ const containerRef = createRef();
+
+ const entityData = clinicalData.clinicalEntities.find(
+ (entity) => entity.entityName === aliasedEntityName,
+ );
+
+ const clinicalErrors = clinicalData.clinicalErrors;
+
+ const { completionStats, entityName, entityFields } = entityData;
+ const showCompletionStats = completionStats && entityName === aliasedEntityNames.donor;
+
+ // totalDocs affects pagination and display text
+ // If using default query, or using search but not filtering by donor in URL, then we display total number of search results
+ // Else we use the total number of results that match our query
+ const totalDocs =
+ (useDefaultQuery && entityType === 'donor') ||
+ (!currentDonors.length && totalResults > entityData.totalDocs)
+ ? totalResults
+ : entityData.totalDocs;
+
+ // iterate for field names not in entity fields
+ // add completion column headers if showing completion stats
+ const columnNames = [
+ ...entityData.records[0]
+ .filter((record) => !entityFields.includes(record.name))
+ .map((record) => record.name),
+ ...(showCompletionStats && Object.values(completionColumnHeaders)),
+ ];
+
+ const records = parseRecords(entityData.records, showCompletionStats, completionStats).sort(
+ sortingFn,
+ );
+
+ const [stickyDonorIDColumnsWidth, setStickyDonorIDColumnsWidth] = useState(74);
+
+ let columns = [];
+ columns = columnNames.map((key) => {
+ return {
+ id: key,
+ accessorKey: key,
+ Header: key,
+ minWidth: getColumnWidth(key, showCompletionStats),
+ };
+ });
+
+ if (showCompletionStats) {
+ columns = [
+ {
+ id: 'clinical_core_completion_header',
+ meta: { customHeader: true },
+ sortingFn,
+ header: () => ,
+
+ columns: columns.slice(0, 7).map((column, index) => ({
+ ...column,
+ sortingFn,
+ header: (props) => {
+ const value = props.header.id;
+ const coreCompletionColumnsCount = 7;
+ const isLastElement = index === coreCompletionColumnsCount - 1;
+ const isSticky = value === 'donor_id';
+ const isSorted = props.sorted;
+
+ return {value} | ;
+ },
+ meta: { customCell: true, customHeader: true },
+ cell: (context) => {
+ const value = context.getValue();
+ const isSticky = context.column.id === 'donor_id';
+
+ const { isCompletionCell, errorState, style } = getCellStyles(
+ undefined,
+ context.row,
+ context.column,
+ showCompletionStats,
+ clinicalErrors,
+ entityType,
+ clinicalData,
+ theme,
+ stickyDonorIDColumnsWidth,
+ );
+
+ const showSuccessSvg = isCompletionCell && !errorState;
+
+ const content = showSuccessSvg ? (
+
+ ) : (
+ value
+ );
+
+ return (
+
+ {content}
+ |
+ );
+ },
+ })),
+ },
+ {
+ id: 'submitted_donor_data_header',
+ meta: { customHeader: true },
+ header: () => (
+
+ ),
+ columns: columns.slice(7),
+ },
+ ];
+ }
+
+ const tableMin = totalDocs > 0 ? page * pageSize + 1 : totalDocs;
+ const tableMax = totalDocs < (page + 1) * pageSize ? totalDocs : (page + 1) * pageSize;
+
+ return (
+
+
+ Showing {tableMin} - {tableMax} of {totalDocs} records
+
+ }
+ />
+
+
+ );
+};
+
+export default ClinicalEntityDataTable;
diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ErrorTable.tsx b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ErrorTable.tsx
new file mode 100644
index 00000000..9929f2ff
--- /dev/null
+++ b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/ErrorTable.tsx
@@ -0,0 +1,118 @@
+import ErrorNotification, { ErrorReportColumns } from '@/app/components/ErrorNotification';
+import { errorNotificationTableProps } from '@/app/components/ErrorNotification/ErrorNotificationDefaultTable';
+import CLINICAL_SCHEMA_VERSION from '@/app/gql/clinical/CLINICAL_SCHEMA_VERSION';
+import { useAppConfigContext } from '@/app/hooks/AppProvider';
+import { useClinicalQuery } from '@/app/hooks/useApolloQuery';
+import { PROGRAM_CLINICAL_SUBMISSION_PATH, PROGRAM_SHORT_NAME_PATH } from '@/global/constants';
+import { ColumnDef, Link, NOTIFICATION_VARIANTS, Table, css } from '@icgc-argo/uikit';
+import urljoin from 'url-join';
+import { clinicalEntityDisplayNames } from '../common';
+
+const errorColumns = [
+ {
+ accessorKey: 'entries',
+ Header: '# Affected Records',
+ id: 'entries',
+ maxWidth: 135,
+ },
+ {
+ accessorKey: 'fieldName',
+ Header: `Field with Error`,
+ id: 'fieldName',
+ maxWidth: 215,
+ },
+ {
+ accessorKey: 'errorMessage',
+ Header: `Error Description`,
+ id: 'errorMessage',
+ },
+];
+
+const Subtitle = ({ program = '' }) => {
+ const { DOCS_URL_ROOT } = useAppConfigContext();
+ const DOCS_DICTIONARY_PAGE = urljoin(DOCS_URL_ROOT, '/dictionary/');
+ const latestDictionaryResponse = useClinicalQuery(CLINICAL_SCHEMA_VERSION);
+
+ return (
+
+
+ {!latestDictionaryResponse.loading &&
+ `Version ${latestDictionaryResponse.data.clinicalSubmissionSchemaVersion}`}
+ {' '}
+ of the data dictionary was released and has made some donors invalid. Please download the
+ error report to view the affected donors, then submit a corrected TSV file in the{' '}
+
+ Submit Clinical Data{' '}
+
+ workspace.
+
+ );
+};
+
+type ErrorTableColumns = {
+ entries: number;
+ errorMessage: string;
+ fieldName: string;
+};
+
+type ErrorTableColumnProperties = {
+ accessorKey: keyof ErrorTableColumns;
+ header: string;
+ maxSize?: number;
+};
+
+type DefaultErrorColumns = {
+ errorReportColumns: ErrorReportColumns[];
+ errorTableColumns: ColumnDef[];
+};
+
+const getErrorColumns = (): DefaultErrorColumns => {
+ const errorTableColumns: ErrorTableColumnProperties[] = [
+ { accessorKey: 'entries', header: '# Affected Records', maxSize: 135 },
+ { accessorKey: 'fieldName', header: `Field with Error`, maxSize: 215 },
+ { accessorKey: 'errorMessage', header: `Error Description` },
+ ];
+
+ const errorReportColumns: ErrorReportColumns[] = errorTableColumns.map(
+ ({ accessorKey, header }) => ({
+ header,
+ id: accessorKey,
+ }),
+ );
+
+ return { errorReportColumns, errorTableColumns };
+};
+
+export type ErrorTableProps = {
+ totalErrorsAmount: number;
+ entityType: string;
+ program: string;
+ tableErrors: any;
+};
+export const ErrorTable = ({
+ totalErrorsAmount,
+ entityType,
+ program,
+ tableErrors,
+}: ErrorTableProps) => {
+ const { errorReportColumns, errorTableColumns } = getErrorColumns();
+
+ return (
+ }
+ reportData={tableErrors}
+ reportColumns={errorReportColumns}
+ tableComponent={
+
+ }
+ />
+ );
+};
diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/usePageSettings.ts b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/usePageSettings.ts
new file mode 100644
index 00000000..bcf19d81
--- /dev/null
+++ b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalDataMain/usePageSettings.ts
@@ -0,0 +1,19 @@
+import { useState } from 'react';
+import { defaultClinicalEntityFilters } from '../common';
+
+export const defaultEntityPageSettings = {
+ page: defaultClinicalEntityFilters.page,
+ pageSize: defaultClinicalEntityFilters.pageSize,
+ sorted: [{ id: 'donorId', desc: true }],
+};
+
+export const defaultDonorSettings = {
+ ...defaultEntityPageSettings,
+ sorted: [{ id: 'completionStats.coreCompletionPercentage', desc: false }],
+};
+
+export const usePageSettings = (defaultState) => {
+ const [pageSettings, setPageSettings] = useState(defaultState);
+ const { page, pageSize, sorted } = pageSettings;
+ return [pageSettings, setPageSettings];
+};
diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalEntityDataTable.tsx b/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalEntityDataTable.tsx
deleted file mode 100644
index 3da8ab91..00000000
--- a/src/app/(post-login)/submission/program/[shortName]/clinical-data/ClinicalEntityDataTable.tsx
+++ /dev/null
@@ -1,759 +0,0 @@
-/*
- * Copyright (c) 2023 The Ontario Institute for Cancer Research. All rights reserved
- *
- * This program and the accompanying materials are made available under the terms of
- * the GNU Affero General Public License v3.0. You should have received a copy of the
- * GNU Affero General Public License along with this program.
- * If not, see .
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
- * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
- * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-import { ClinicalSearchResults } from '@/__generated__/clinical/graphql';
-import ErrorNotification from '@/app/components/ErrorNotification';
-import { TableInfoHeaderContainer } from '@/app/components/Table/common';
-import CLINICAL_ENTITY_DATA_QUERY from '@/app/gql/clinical/CLINICAL_ENTITY_DATA_QUERY';
-import CLINICAL_SCHEMA_VERSION from '@/app/gql/clinical/CLINICAL_SCHEMA_VERSION';
-import { useAppConfigContext } from '@/app/hooks/AppProvider';
-import { useClinicalQuery } from '@/app/hooks/useApolloQuery';
-import { PROGRAM_CLINICAL_SUBMISSION_PATH, PROGRAM_SHORT_NAME_PATH } from '@/global/constants';
-import {
- ContentPlaceholder,
- DnaLoader,
- Icon,
- Link,
- NOTIFICATION_VARIANTS,
- Table,
- Typography,
- css,
- useTheme,
-} from '@icgc-argo/uikit';
-import memoize from 'lodash/memoize';
-import { createRef, useEffect, useState } from 'react';
-import urljoin from 'url-join';
-import {
- Cell,
- ClinicalCoreCompletionHeader,
- TopLevelHeader,
- styleThickBorder,
-} from './ClinicalDataTableComp';
-import {
- ClinicalEntitySearchResultResponse,
- CompletionStates,
- aliasSortNames,
- aliasedEntityFields,
- aliasedEntityNames,
- clinicalEntityDisplayNames,
- clinicalEntityFields,
- defaultClinicalEntityFilters,
- emptyClinicalDataResponse,
- emptySearchResponse,
-} from './common';
-
-export type DonorEntry = {
- row: string;
- isNew: boolean;
- [k: string]: string | number | boolean;
-};
-
-const errorColumns = [
- {
- accessorKey: 'entries',
- Header: '# Affected Records',
- id: 'entries',
- maxWidth: 135,
- },
- {
- accessorKey: 'fieldName',
- Header: `Field with Error`,
- id: 'fieldName',
- maxWidth: 215,
- },
- {
- accessorKey: 'errorMessage',
- Header: `Error Description`,
- id: 'errorMessage',
- },
-];
-
-const NoDataCell = () => (
-
-
-
-
-
-);
-
-const completionKeys = Object.values(aliasSortNames);
-const completionColumnNames = Object.keys(aliasSortNames);
-const emptyCompletion = {
- DO: 0,
- PD: 0,
- FO: 0,
- NS: 0,
- TR: 0,
- TS: 0,
-};
-
-const noDataCompletionStats = [
- {
- donor_id: 0,
- ...emptyCompletion,
- },
-];
-
-const completionColumnHeaders = {
- donor: 'DO',
- primaryDiagnosis: 'PD',
- normalSpecimens: 'NS',
- tumourSpecimens: 'TS',
- treatments: 'TR',
- followUps: 'FO',
-};
-
-const coreCompletionFields = Object.keys(completionColumnHeaders);
-
-const getColumnWidth = memoize<
- (keyString: string, showCompletionStats: boolean, noData: boolean) => number
->((keyString, showCompletionStats, noData) => {
- const minWidth = keyString === 'donor_id' ? 70 : showCompletionStats ? 40 : 95;
- const maxWidth = noData && showCompletionStats ? 45 : 200;
- const spacePerChar = 8;
- const margin = 10;
- const targetWidth = keyString.length * spacePerChar + margin;
- return Math.max(Math.min(maxWidth, targetWidth), minWidth);
-});
-
-const defaultEntityPageSettings = {
- page: defaultClinicalEntityFilters.page,
- pageSize: defaultClinicalEntityFilters.pageSize,
- sorted: [{ id: 'donorId', desc: true }],
-};
-
-const defaultDonorSettings = {
- ...defaultEntityPageSettings,
- sorted: [{ id: 'completionStats.coreCompletionPercentage', desc: false }],
-};
-
-const defaultErrorPageSettings = {
- page: 0,
- pageSize: 5,
- sorted: [{ id: 'donorId', desc: true }],
-};
-
-const validateEntityQueryName = (entityQuery) => {
- const entities = typeof entityQuery === 'string' ? [entityQuery] : entityQuery;
- return entities.map((entityName) => clinicalEntityFields.find((entity) => entity === entityName));
-};
-
-export const useGetEntityData = (
- program: string,
- entityType: string | string[],
- page: number,
- pageSize: number,
- sort: string,
- completionState: CompletionStates,
- donorIds: number[],
- submitterDonorIds: string[],
-) =>
- useClinicalQuery(CLINICAL_ENTITY_DATA_QUERY, {
- errorPolicy: 'all',
- fetchPolicy: 'cache-and-network',
- variables: {
- programShortName: program,
- filters: {
- ...defaultClinicalEntityFilters,
- sort,
- page,
- pageSize,
- completionState,
- donorIds,
- submitterDonorIds,
- entityTypes: validateEntityQueryName(entityType),
- },
- },
- });
-
-const ClinicalEntityDataTable = ({
- entityType,
- program,
- completionState = CompletionStates['all'],
- currentDonors,
- donorSearchResults = emptySearchResponse,
- useDefaultQuery,
- noData,
-}: {
- entityType: string;
- program: string;
- completionState: CompletionStates;
- currentDonors: number[];
- donorSearchResults: ClinicalEntitySearchResultResponse;
- useDefaultQuery: boolean;
- noData: boolean;
-}) => {
- const { DOCS_URL_ROOT } = useAppConfigContext();
- const DOCS_DICTIONARY_PAGE = urljoin(DOCS_URL_ROOT, '/dictionary/');
-
- // Init + Page Settings
- let totalDocs = 0;
- let showCompletionStats = false;
- let records = [];
- let columns = [];
- const theme = useTheme();
- const containerRef = createRef();
- const defaultPageSettings =
- useDefaultQuery && entityType === 'donor' ? defaultDonorSettings : defaultEntityPageSettings;
- const [pageSettings, setPageSettings] = useState(defaultPageSettings);
- const { page, pageSize, sorted } = pageSettings;
- const [errorPageSettings, setErrorPageSettings] = useState(defaultErrorPageSettings);
- const { page: errorPage, pageSize: errorPageSize, sorted: errorSorted } = errorPageSettings;
- const { desc, id } = sorted[0];
- const sortKey = aliasSortNames[id] || id;
- const sort = `${desc ? '-' : ''}${sortKey}`;
-
- const {
- clinicalSearchResults: { searchResults, totalResults },
- } = donorSearchResults || emptySearchResponse;
-
- const nextSearchPage = (page + 1) * pageSize;
-
- const donorIds = useDefaultQuery
- ? []
- : currentDonors.length
- ? currentDonors
- : searchResults.map(({ donorId }: ClinicalSearchResults) => donorId);
-
- const submitterDonorIds =
- useDefaultQuery || currentDonors.length
- ? []
- : searchResults
- .map(({ submitterDonorId }: ClinicalSearchResults) => submitterDonorId)
- .filter((id) => !!id)
- .slice(page * pageSize, nextSearchPage < totalResults ? nextSearchPage : totalResults);
-
- const latestDictionaryResponse = useClinicalQuery(CLINICAL_SCHEMA_VERSION);
- const Subtitle = ({ program = '' }) => (
-
-
- {!latestDictionaryResponse.loading &&
- `Version ${latestDictionaryResponse.data.clinicalSubmissionSchemaVersion}`}
- {' '}
- of the data dictionary was released and has made some donors invalid. Please download the
- error report to view the affected donors, then submit a corrected TSV file in the{' '}
-
- Submit Clinical Data{' '}
-
- workspace.
-
- );
-
- const updatePageSettings = (key, value) => {
- const newPageSettings = { ...pageSettings, [key]: value };
-
- if (key === 'pageSize' && value !== pageSettings.pageSize) {
- // Prevents bug querying nonexistent data
- newPageSettings.page = 0;
- }
- setPageSettings(newPageSettings);
- return newPageSettings;
- };
-
- useEffect(() => {
- setPageSettings(defaultPageSettings);
- setErrorPageSettings(defaultErrorPageSettings);
- }, [entityType, useDefaultQuery]);
-
- const { data: clinicalEntityData, loading } = useGetEntityData(
- program,
- entityType,
- page,
- pageSize,
- sort,
- completionState,
- donorIds,
- submitterDonorIds,
- );
-
- const { clinicalData } =
- clinicalEntityData == undefined || loading ? emptyClinicalDataResponse : clinicalEntityData;
-
- const noTableData = noData || clinicalData.clinicalEntities.length === 0;
-
- // Collect Error Data
- const { clinicalErrors = [] } = clinicalData;
- const tableErrorGroups = [];
-
- clinicalErrors.forEach((donor) => {
- const relatedErrors = donor.errors.filter(
- (error) => error.entityName === aliasedEntityNames[entityType],
- );
-
- relatedErrors.forEach((error) => {
- const { donorId } = donor;
- const { errorType, message, fieldName } = error;
- const relatedErrorGroup = tableErrorGroups.find(
- (tableErrorGroup) =>
- tableErrorGroup[0].errorType === errorType &&
- tableErrorGroup[0].message === message &&
- tableErrorGroup[0].fieldName === fieldName,
- );
- const tableError = { ...error, donorId };
-
- if (!relatedErrorGroup) {
- tableErrorGroups.push([tableError]);
- } else {
- relatedErrorGroup.push(tableError);
- }
- });
- });
-
- const tableErrors = tableErrorGroups.map((errorGroup) => {
- // Counts Number of Records affected for each Error Object
- const { fieldName, entityName, message, errorType } = errorGroup[0];
-
- const errorMessage =
- errorType === 'UNRECOGNIZED_FIELD'
- ? `${fieldName} is not a field within the latest dictionary. Please remove this from the ${entityName}.tsv file before submitting.`
- : message;
-
- const entries = errorGroup.length;
-
- return {
- entries,
- fieldName,
- entityName,
- errorMessage,
- };
- });
-
- const totalErrors = tableErrors.reduce(
- (errorCount, errorGroup) => errorCount + errorGroup.entries,
- 0,
- );
- const hasErrors = totalErrors > 0;
-
- const sortEntityData = (prev, next) => {
- let sortVal = 0;
-
- if (hasErrors) {
- // If Current Entity has Errors, Prioritize Data w/ Errors
- const { errorsA, errorsB } = clinicalErrors.reduce(
- (acc, current) => {
- if (current.donorId == prev['donor_id']) {
- acc.errorsA = -1;
- }
- if (current.donorId == next['donor_id']) {
- acc.errorsB = 1;
- }
- return acc;
- },
- { errorsA: 0, errorsB: 0 },
- );
-
- sortVal += errorsA + errorsB;
- }
-
- // Handles Manual User Sorting by Core Completion columns
- const completionSortIndex = completionKeys.indexOf(sortKey);
-
- if (completionSortIndex) {
- const completionSortKey = completionColumnNames[completionSortIndex];
- const completionA = prev[completionSortKey];
- const completionB = next[completionSortKey];
-
- sortVal = completionA === completionB ? 0 : completionA > completionB ? -1 : 1;
- sortVal *= desc ? -1 : 1;
- }
-
- return sortVal;
- };
-
- // Map Completion Stats + Entity Data
- if (noTableData) {
- showCompletionStats = true;
- records = noDataCompletionStats;
- } else {
- const entityData = clinicalData.clinicalEntities.find(
- (entity) => entity.entityName === aliasedEntityNames[entityType],
- );
- columns = [...entityData.entityFields];
- const { completionStats, entityName } = entityData;
- showCompletionStats = !!(completionStats && entityName === aliasedEntityNames.donor);
-
- // totalDocs affects pagination and display text
- // If using default query, or using search but not filtering by donor in URL, then we display total number of search results
- // Else we use the total number of results that match our query
- totalDocs =
- (useDefaultQuery && entityType === 'donor') ||
- (!currentDonors.length && totalResults > entityData.totalDocs)
- ? totalResults
- : entityData.totalDocs;
-
- entityData.records.forEach((record) => {
- record.forEach((r) => {
- if (!columns.includes(r.name)) columns.push(r.name);
- });
- });
- if (showCompletionStats) {
- columns.splice(1, 0, ...Object.values(completionColumnHeaders));
- }
-
- records = entityData.records
- .map((record) => {
- let clinicalRecord = {};
- record.forEach((r) => {
- const displayKey = r.name;
- clinicalRecord[displayKey] = displayKey === 'donor_id' ? `DO${r.value}` : r.value || '';
- if (showCompletionStats && displayKey === 'donor_id') {
- const completionRecord = completionStats.find(
- (stat) => stat.donorId === parseInt(r.value),
- );
-
- if (!completionRecord) {
- clinicalRecord = { ...clinicalRecord, ...emptyCompletion };
- } else {
- const { coreCompletion, entityData: completionEntityData } = completionRecord;
-
- coreCompletionFields.forEach((field) => {
- const completionField = completionColumnHeaders[field];
- const isSpecimenField =
- completionField === completionColumnHeaders['normalSpecimens'] ||
- completionField === completionColumnHeaders['tumourSpecimens'];
-
- if (!isSpecimenField) {
- const completionValue = coreCompletion[field];
- clinicalRecord[completionField] = completionValue || 0;
- } else {
- const {
- specimens: {
- coreCompletionPercentage,
- normalSpecimensPercentage,
- tumourSpecimensPercentage,
- normalSubmissions,
- tumourSubmissions,
- },
- } = completionEntityData;
-
- if (coreCompletionPercentage === 1) {
- clinicalRecord[completionField] = 1;
- } else {
- const currentPercentage =
- completionField === completionColumnHeaders['normalSpecimens']
- ? normalSpecimensPercentage
- : tumourSpecimensPercentage;
- const currentSubmissions =
- completionField === completionColumnHeaders['normalSpecimens']
- ? normalSubmissions
- : tumourSubmissions;
- const hasErrors = currentPercentage !== 1;
- const value = hasErrors ? currentSubmissions : currentPercentage;
- clinicalRecord[completionField] = value;
- }
- }
- });
- }
- }
- });
-
- return clinicalRecord;
- })
- .sort(sortEntityData);
- }
-
- const getHeaderBorder = (key) =>
- (showCompletionStats && key === completionColumnHeaders.followUps) ||
- (!showCompletionStats && key === 'donor_id') ||
- key === 'FO'
- ? styleThickBorder
- : '';
-
- const [stickyDonorIDColumnsWidth, setStickyDonorIDColumnsWidth] = useState(74);
-
- const getCellStyles = (state, row, column) => {
- const { original } = row;
- const { id } = column;
- const isCompletionCell =
- showCompletionStats && Object.values(completionColumnHeaders).includes(id);
-
- const isSpecimenCell =
- isCompletionCell &&
- (id === completionColumnHeaders.normalSpecimens ||
- id === completionColumnHeaders.tumourSpecimens);
-
- const originalDonorId = original['donor_id'];
- const cellDonorId = parseInt(
- originalDonorId && originalDonorId.includes('DO')
- ? originalDonorId.substring(2)
- : originalDonorId,
- );
-
- const donorErrorData = clinicalErrors
- .filter((donor) => donor.donorId === cellDonorId)
- .map((donor) => donor.errors)
- .flat();
-
- const columnErrorData =
- donorErrorData.length &&
- donorErrorData.filter(
- (error) =>
- error &&
- (error.entityName === entityType ||
- (aliasedEntityFields.includes(error.entityName) &&
- aliasedEntityNames[entityType] === error.entityName)) &&
- error.fieldName === id,
- );
-
- const hasClinicalErrors = columnErrorData && columnErrorData.length >= 1;
-
- let hasCompletionErrors = isCompletionCell && original[id] !== 1;
-
- if (isSpecimenCell) {
- const completionData = clinicalData.clinicalEntities.find(
- (entity) => entity.entityName === aliasedEntityNames['donor'],
- ).completionStats;
-
- const completionRecord =
- isCompletionCell &&
- completionData.find((stat) => stat.donorId === parseInt(originalDonorId.substr(2)));
-
- if (completionRecord) {
- const { entityData: completionEntityData } = completionRecord;
-
- const {
- specimens: { normalSpecimensPercentage, tumourSpecimensPercentage },
- } = completionEntityData;
-
- const currentPercentage =
- id === completionColumnHeaders['normalSpecimens']
- ? normalSpecimensPercentage
- : tumourSpecimensPercentage;
-
- hasCompletionErrors = currentPercentage !== 1;
- }
- }
-
- const specificErrorValue =
- hasClinicalErrors &&
- columnErrorData.filter(
- (error) =>
- (error.errorType === 'INVALID_BY_SCRIPT' || error.errorType === 'INVALID_ENUM_VALUE') &&
- (error.info?.value === original[id] ||
- (error.info?.value && error.info.value[0] === original[id]) ||
- (error.info.value === null && !Boolean(original[id]))),
- );
-
- const fieldError =
- hasClinicalErrors &&
- columnErrorData.filter(
- (error) =>
- (error.errorType === 'UNRECOGNIZED_FIELD' ||
- error.errorType === 'MISSING_REQUIRED_FIELD') &&
- error.fieldName === id,
- );
-
- const errorState =
- // Completion Stats === 1 indicates Complete
- // 0 is Incomplete, <1 Incorrect Sample / Specimen Ratio
- (isCompletionCell && hasCompletionErrors) ||
- specificErrorValue?.length > 0 ||
- fieldError?.length > 0;
-
- // use Emotion styling
- const headerDonorIdStyle = css`
- background: white,
- position: absolute,
- `;
- const stickyMarginStyle = css`
- margin-left: ${stickyDonorIDColumnsWidth};
- `;
- const style = css`
- color: ${isCompletionCell && !errorState && theme.colors.accent1_dark};
- background: ${errorState && theme.colors.error_4};
- ${getHeaderBorder(id)}
- ${column.Header === 'donor_id' && headerDonorIdStyle};
- ${column.Header === 'DO' && stickyMarginStyle};
- ${column.Header === 'program_id' && !showCompletionStats && stickyMarginStyle};
- `;
-
- return {
- style,
- isCompletionCell,
- errorState,
- };
- };
-
- columns = columns.map((key) => {
- return {
- id: key,
- accessorKey: key,
- Header: key,
- minWidth: getColumnWidth(key, showCompletionStats, noTableData),
- };
- });
-
- if (showCompletionStats) {
- columns = [
- {
- id: 'clinical_core_completion_header',
- meta: { customHeader: true },
- sortingFn: sortEntityData,
- header: () => ,
-
- columns: columns.slice(0, 7).map((column, index) => ({
- ...column,
- sortingFn: sortEntityData,
- header: (props) => {
- const value = props.header.id;
- const coreCompletionColumnsCount = 7;
- const isLastElement = index === coreCompletionColumnsCount - 1;
- const isSticky = value === 'donor_id';
- const isSorted = props.sorted;
-
- return {value} | ;
- },
- meta: { customCell: true, customHeader: true },
- cell: (context) => {
- const value = context.getValue();
- const isSticky = context.column.id === 'donor_id';
-
- const { isCompletionCell, errorState, style } = getCellStyles(
- undefined,
- context.row,
- context.column,
- );
-
- const showSuccessSvg = isCompletionCell && !errorState;
-
- const content = showSuccessSvg ? (
-
- ) : (
- value
- );
-
- return (
-
- {content}
- |
- );
- },
- })),
- },
- {
- id: 'submitted_donor_data_header',
- meta: { customHeader: true },
- header: () => (
-
- ),
- columns: columns.slice(7),
- },
- ];
- }
-
- const tableMin = totalDocs > 0 ? page * pageSize + 1 : totalDocs;
- const tableMax = totalDocs < (page + 1) * pageSize ? totalDocs : (page + 1) * pageSize;
- const numTablePages = Math.ceil(totalDocs / pageSize);
- const numErrorPages = Math.ceil(totalErrors / errorPageSize);
-
- return loading ? (
-
- ) : noTableData ? (
-
- ) : (
-
- {hasErrors && (
-
- }
- errors={tableErrors}
- columnConfig={errorColumns}
- tableProps={{
- page: errorPage,
- pages: numErrorPages,
- pageSize: errorPageSize,
- sorted: errorSorted,
- onPageChange: (value) => updatePageSettings('page', value),
- onPageSizeChange: (value) => updatePageSettings('pageSize', value),
- onSortedChange: (value) => updatePageSettings('sorted', value),
- // TODO: Test + Update Pagination in #2267
- // https://github.com/icgc-argo/platform-ui/issues/2267
- showPagination: false,
- }}
- />
-
- )}
-
-
- Showing {tableMin} - {tableMax} of {totalDocs} records
-
- }
- />
-
-
- );
-};
-
-export default ClinicalEntityDataTable;
diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/page.tsx b/src/app/(post-login)/submission/program/[shortName]/clinical-data/page.tsx
index 383aeb67..70ec9f7f 100644
--- a/src/app/(post-login)/submission/program/[shortName]/clinical-data/page.tsx
+++ b/src/app/(post-login)/submission/program/[shortName]/clinical-data/page.tsx
@@ -27,11 +27,9 @@ import { useClinicalQuery } from '@/app/hooks/useApolloQuery';
import useUrlParamState from '@/app/hooks/useUrlParamState';
import { notNull, parseDonorIdString } from '@/global/utils';
import { css } from '@/lib/emotion';
-import { useQuery } from '@apollo/client';
import { Container, Loader, Typography, VerticalTabs, useTheme } from '@icgc-argo/uikit';
import { useState } from 'react';
import { setConfiguration } from 'react-grid-system';
-import ClinicalEntityDataTable from './ClinicalEntityDataTable';
import ClinicalDownloadButton from './DownloadButtons';
import SearchBar from './SearchBar';
import {
@@ -137,7 +135,7 @@ const ClinicalDataPageComp = ({ programShortName }: { programShortName: string }
// Side Menu Query
// Populates Clinical Entity Table, Side Menu, Title Bar
- const { data: sideMenuQuery, loading: sideMenuLoading } = useQuery(
+ const { data: sideMenuQuery, loading: sideMenuLoading } = useClinicalQuery(
SUBMITTED_DATA_SIDE_MENU_QUERY,
{
errorPolicy: 'all',
@@ -178,21 +176,23 @@ const ClinicalDataPageComp = ({ programShortName }: { programShortName: string }
submitterDonorIds: useDefaultQuery ? [] : entityTableSubmitterDonorIds.filter(notNull),
};
- const menuItems = clinicalEntityFields.map((entity) => (
- setSelectedClinicalEntityTab(aliasedEntityNames[entity])}
- disabled={
- !clinicalData.clinicalEntities.some((e) => e?.entityName === aliasedEntityNames[entity])
- }
- >
- {clinicalEntityDisplayNames[entity]}
- {hasClinicalErrors(clinicalData, entity) && (
- !
- )}
-
- ));
+ const menuItems = clinicalEntityFields.map((entity) => {
+ const aliasedEntityName = aliasedEntityNames[entity];
+
+ return (
+ setSelectedClinicalEntityTab(aliasedEntityName)}
+ disabled={!clinicalData.clinicalEntities.some((e) => e.entityName === aliasedEntityName)}
+ >
+ {clinicalEntityDisplayNames[entity]}
+ {hasClinicalErrors(clinicalData, entity) && (
+ !
+ )}
+
+ );
+ });
return (
@@ -279,7 +279,7 @@ const ClinicalDataPageComp = ({ programShortName }: { programShortName: string }
margin-top: 16px;
`}
>
-
+ /> */}
{' '}
diff --git a/src/app/(post-login)/submission/program/[shortName]/clinical-data/tableDataRefactor.tsx b/src/app/(post-login)/submission/program/[shortName]/clinical-data/tableDataRefactor.tsx
new file mode 100644
index 00000000..104676e2
--- /dev/null
+++ b/src/app/(post-login)/submission/program/[shortName]/clinical-data/tableDataRefactor.tsx
@@ -0,0 +1,79 @@
+/**
+ * Reduce errors across all records into an object detailing:
+ * - which fields have an error, the error description and how many rows are affected
+ * example: if there are 7 records with fields of "cancer_type_code" that have the error MISSING_REQUIRED_FIELD
+ * return: something like {affectedfields: 7, error_field: cancer_type_code, error_message: "cancer_type_code is a required field"}
+ **/
+
+export const formatTableErrors = ({ clinicalErrors, aliasedEntityName }) => {
+ const tableErrorGroups = [];
+ // {
+ // "donorId": 262500,
+ // "submitterDonorId": "Pat-1",
+ // "errors": [
+ // {
+ // "errorType": "INVALID_BY_SCRIPT",
+ // "fieldName": "lymph_nodes_examined_method",
+ // "index": 0,
+ // "info": {
+ // "value": null,
+ // "__typename": "ClinicalErrorInfo"
+ // },
+ // "message": "The 'lymph_nodes_examined_method' field must be submitted if the 'lymph_nodes_examined_status' field is 'Yes'",
+ // "entityName": "primary_diagnosis",
+ // "__typename": "ClinicalErrorRecord"
+ // }]}
+ // [{donorId..., errors: [{....all primary diag}]},{donorId..., errors: [{....all donor}]}] etc
+ // no need to loop over every single error because they are grouped already
+ clinicalErrors.forEach((donor) => {
+ const relatedErrors = donor.errors.filter((error) => error.entityName === aliasedEntityName);
+ console.log('realted', relatedErrors);
+ relatedErrors.forEach((error) => {
+ const { donorId } = donor;
+ const { errorType, message, fieldName } = error;
+ const relatedErrorGroup = tableErrorGroups.find(
+ (tableErrorGroup) =>
+ tableErrorGroup[0].errorType === errorType &&
+ tableErrorGroup[0].message === message &&
+ tableErrorGroup[0].fieldName === fieldName,
+ );
+ const tableError = { ...error, donorId };
+
+ if (!relatedErrorGroup) {
+ tableErrorGroups.push([tableError]);
+ } else {
+ relatedErrorGroup.push(tableError);
+ }
+ });
+ });
+
+ console.log('table error groups', tableErrorGroups);
+
+ const tableErrors = tableErrorGroups.map((errorGroup) => {
+ // Counts Number of Records affected for each Error Object
+ const { fieldName, entityName, message, errorType } = errorGroup[0];
+
+ const errorMessage =
+ errorType === 'UNRECOGNIZED_FIELD'
+ ? `${fieldName} is not a field within the latest dictionary. Please remove this from the ${entityName}.tsv file before submitting.`
+ : message;
+
+ const entries = errorGroup.length;
+
+ return {
+ entries,
+ fieldName,
+ entityName,
+ errorMessage,
+ };
+ });
+
+ const totalErrorsAmount = tableErrors.reduce(
+ (errorCount, errorGroup) => errorCount + errorGroup.entries,
+ 0,
+ );
+
+ return { tableErrors, totalErrorsAmount };
+};
+
+//