• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CBIIT / crdc-datahub-ui / 20439596599

22 Dec 2025 05:43PM UTC coverage: 78.972% (+0.8%) from 78.178%
20439596599

push

github

web-flow
Merge pull request #924 from CBIIT/3.5.0

3.5.0 Release

5158 of 5669 branches covered (90.99%)

Branch coverage included in aggregate %.

806 of 844 new or added lines in 46 files covered. (95.5%)

9 existing lines in 5 files now uncovered.

30666 of 39694 relevant lines covered (77.26%)

231.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.41
/src/content/dataSubmissions/QualityControl.tsx
1
import { useLazyQuery, useQuery } from "@apollo/client";
1✔
2
import { Box, Button, Stack, styled, TableCell } from "@mui/material";
1✔
3
import { isEqual } from "lodash";
1✔
4
import { useSnackbar } from "notistack";
1✔
5
import React, {
1✔
6
  FC,
7
  MutableRefObject,
8
  useCallback,
9
  useEffect,
10
  useMemo,
11
  useRef,
12
  useState,
13
} from "react";
14

15
import { TOOLTIP_TEXT } from "@/config/DashboardTooltips";
1✔
16

17
import { useSubmissionContext } from "../../components/Contexts/SubmissionContext";
1✔
18
import { ExportValidationButton } from "../../components/DataSubmissions/ExportValidationButton";
1✔
19
import QualityControlFilters from "../../components/DataSubmissions/QualityControlFilters";
1✔
20
import DoubleLabelSwitch from "../../components/DoubleLabelSwitch";
1✔
21
import ErrorDetailsDialog, { ErrorDetailsIssue } from "../../components/ErrorDetailsDialog/v2";
1✔
22
import GenericTable, { Column } from "../../components/GenericTable";
1✔
23
import NodeComparison from "../../components/NodeComparison";
1✔
24
import PVRequestButton from "../../components/PVRequestButton";
1✔
25
import StyledTooltip from "../../components/StyledFormComponents/StyledTooltip";
1✔
26
import TruncatedText from "../../components/TruncatedText";
1✔
27
import { ValidationErrorCodes } from "../../config/ValidationErrors";
1✔
28
import {
1✔
29
  AGGREGATED_SUBMISSION_QC_RESULTS,
30
  SUBMISSION_QC_RESULTS,
31
  AggregatedSubmissionQCResultsInput,
32
  AggregatedSubmissionQCResultsResp,
33
  SubmissionQCResultsInput,
34
  SubmissionQCResultsResp,
35
  GET_PENDING_PVS,
36
  GetPendingPVsInput,
37
  GetPendingPVsResponse,
38
} from "../../graphql";
39
import { FormatDate, Logger, titleCase } from "../../utils";
1✔
40

41
import QCResultsContext from "./Contexts/QCResultsContext";
1✔
42

43
type FilterForm = {
44
  issueType: string;
45
  /**
46
   * The node type to filter by.
47
   *
48
   * @default "All"
49
   */
50
  nodeType: string;
51
  batchID: number | "All";
52
  severity: QCResult["severity"] | "All";
53
};
54

55
const StyledErrorDetailsButton = styled(Button)({
1✔
56
  display: "inline",
1✔
57
  color: "#0B6CB1",
1✔
58
  fontFamily: "'Nunito', 'Rubik', sans-serif",
1✔
59
  fontSize: "16px",
1✔
60
  fontStyle: "normal",
1✔
61
  fontWeight: 600,
1✔
62
  lineHeight: "19px",
1✔
63
  padding: 0,
1✔
64
  textDecorationLine: "underline",
1✔
65
  textTransform: "none",
1✔
66
  marginLeft: "auto",
1✔
67
  paddingLeft: "8px",
1✔
68
  "&:hover": {
1✔
69
    background: "transparent",
1✔
70
    textDecorationLine: "underline",
1✔
71
  },
1✔
72
});
1✔
73

74
const StyledNodeType = styled(Box)({
1✔
75
  display: "flex",
1✔
76
  alignItems: "center",
1✔
77
  textTransform: "capitalize",
1✔
78
});
1✔
79

80
const StyledSeverity = styled(Box)({
1✔
81
  display: "flex",
1✔
82
  alignItems: "center",
1✔
83
});
1✔
84

85
const StyledBreakAll = styled(Box)({
1✔
86
  wordBreak: "break-all",
1✔
87
});
1✔
88

89
const StyledIssuesTextWrapper = styled(Box)({
1✔
90
  whiteSpace: "nowrap",
1✔
91
  wordBreak: "break-word",
1✔
92
});
1✔
93

94
const StyledDateTooltip = styled(StyledTooltip)(() => ({
1✔
95
  cursor: "pointer",
8✔
96
}));
1✔
97

98
const StyledPvButtonWrapper = styled(Box)({
1✔
99
  marginLeft: "89px",
1✔
100
});
1✔
101

102
const StyledOthersText = styled("span")({
1✔
103
  display: "inline",
1✔
104
  textDecoration: "underline",
1✔
105
  cursor: "pointer",
1✔
106
  color: "#0B6CB1",
1✔
107
  whiteSpace: "nowrap",
1✔
108
  fontSize: "14px",
1✔
109
  fontStyle: "normal",
1✔
110
  fontWeight: 600,
1✔
111
  lineHeight: "19px",
1✔
112
});
1✔
113

114
const StyledHeaderCell = styled(TableCell)({
1✔
115
  fontWeight: 700,
1✔
116
  fontSize: "14px",
1✔
117
  lineHeight: "16px",
1✔
118
  color: "#fff !important",
1✔
119
  padding: "22px 4px",
1✔
120
  verticalAlign: "top",
1✔
121
  "&.MuiTableCell-root:first-of-type": {
1✔
122
    paddingTop: "22px",
1✔
123
    paddingRight: "4px",
1✔
124
    paddingBottom: "22px",
1✔
125
    color: "#fff !important",
1✔
126
  },
1✔
127
  "& .MuiSvgIcon-root, & .MuiButtonBase-root": {
1✔
128
    color: "#fff !important",
1✔
129
  },
1✔
130
});
1✔
131

132
const StyledTableCell = styled(TableCell)({
1✔
133
  fontSize: "14px",
1✔
134
  color: "#083A50 !important",
1✔
135
  "&.MuiTableCell-root": {
1✔
136
    padding: "14px 4px 12px",
1✔
137
    overflowWrap: "anywhere",
1✔
138
    whiteSpace: "nowrap",
1✔
139
  },
1✔
140
  "&:last-of-type": {
1✔
141
    paddingRight: "4px",
1✔
142
  },
1✔
143
});
1✔
144

145
type RowData = QCResult | AggregatedQCResult;
146

147
const aggregatedColumns: Column<AggregatedQCResult>[] = [
1✔
148
  {
1✔
149
    label: "Issue Type",
1✔
150
    renderValue: (data) => <TruncatedText text={data.title} maxCharacters={50} />,
1✔
151
    field: "title",
1✔
152
  },
1✔
153
  {
1✔
154
    label: "Property",
1✔
155
    renderValue: (data) => <TruncatedText text={data.property} maxCharacters={50} />,
1✔
156
    field: "property",
1✔
157
  },
1✔
158
  {
1✔
159
    label: "Value",
1✔
160
    renderValue: (data) => <TruncatedText text={data.value} maxCharacters={50} />,
1✔
161
    field: "value",
1✔
162
  },
1✔
163
  {
1✔
164
    label: "Severity",
1✔
165
    renderValue: (data) => (
1✔
166
      <StyledSeverity color={data?.severity === "Error" ? "#B54717" : "#8D5809"}>
2✔
167
        {data?.severity}
2✔
168
      </StyledSeverity>
2✔
169
    ),
170
    field: "severity",
1✔
171
  },
1✔
172
  {
1✔
173
    label: "Record Count",
1✔
174
    renderValue: (data) =>
1✔
175
      Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(data.count || 0),
2!
176
    field: "count",
1✔
177
    default: true,
1✔
178
  },
1✔
179
  {
1✔
180
    label: "Expand",
1✔
181
    renderValue: (data) => (
1✔
182
      <QCResultsContext.Consumer>
2✔
183
        {({ handleExpandClick }) => (
2✔
184
          <StyledErrorDetailsButton
2✔
185
            onClick={() => handleExpandClick?.(data)}
2✔
186
            variant="text"
2✔
187
            disableRipple
2✔
188
            disableTouchRipple
2✔
189
            disableFocusRipple
2✔
190
          >
191
            Expand
192
          </StyledErrorDetailsButton>
2✔
193
        )}
194
      </QCResultsContext.Consumer>
2✔
195
    ),
196
    fieldKey: "expand",
1✔
197
    sortDisabled: true,
1✔
198
    sx: {
1✔
199
      width: "104px",
1✔
200
      textAlign: "center",
1✔
201
    },
1✔
202
  },
1✔
203
];
204

205
const expandedColumns: Column<QCResult>[] = [
1✔
206
  {
1✔
207
    label: "Batch ID",
1✔
208
    renderValue: (data) => <StyledBreakAll>{data?.displayID}</StyledBreakAll>,
1✔
209
    field: "displayID",
1✔
210
    default: true,
1✔
211
    sx: {
1✔
212
      width: "110px",
1✔
213
    },
1✔
214
  },
1✔
215
  {
1✔
216
    label: "Node Type",
1✔
217
    renderValue: (data) => (
1✔
218
      <StyledNodeType>
18✔
219
        <TruncatedText text={data?.type} maxCharacters={12} disableInteractiveTooltip={false} />
18✔
220
      </StyledNodeType>
18✔
221
    ),
222
    field: "type",
1✔
223
  },
1✔
224
  {
1✔
225
    label: "Submitted Identifier",
1✔
226
    renderValue: (data) => (
1✔
227
      <TruncatedText
18✔
228
        text={data?.submittedID}
18✔
229
        maxCharacters={15}
18✔
230
        disableInteractiveTooltip={false}
18✔
231
      />
232
    ),
233
    field: "submittedID",
1✔
234
  },
1✔
235
  {
1✔
236
    label: "Severity",
1✔
237
    renderValue: (data) => (
1✔
238
      <StyledSeverity color={data?.severity === "Error" ? "#B54717" : "#8D5809"}>
18✔
239
        {data?.severity}
18✔
240
      </StyledSeverity>
18✔
241
    ),
242
    field: "severity",
1✔
243
    sx: {
1✔
244
      width: "87px",
1✔
245
    },
1✔
246
  },
1✔
247
  {
1✔
248
    label: "Validated Date",
1✔
249
    renderValue: (data) =>
1✔
250
      data.validatedDate ? (
18✔
251
        <StyledDateTooltip
10✔
252
          title={FormatDate(data.validatedDate, "M/D/YYYY h:mm A")}
10✔
253
          placement="top"
10✔
254
        >
255
          <span>{FormatDate(data.validatedDate, "M/D/YYYY")}</span>
10✔
256
        </StyledDateTooltip>
10✔
257
      ) : (
258
        ""
8✔
259
      ),
260
    field: "validatedDate",
1✔
261
    sx: {
1✔
262
      width: "132px",
1✔
263
    },
1✔
264
  },
1✔
265
  {
1✔
266
    label: "Issue Count",
1✔
267
    renderValue: (data) =>
1✔
268
      Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(data.issueCount || 0),
18✔
269
    field: "issueCount",
1✔
270
    sx: {
1✔
271
      width: "110px",
1✔
272
    },
1✔
273
  },
1✔
274
  {
1✔
275
    label: "Issue(s)",
1✔
276
    renderValue: (data) =>
1✔
277
      (data?.errors?.length > 0 || data?.warnings?.length > 0) && (
18✔
278
        <QCResultsContext.Consumer>
10✔
279
          {({ handleOpenErrorDialog }) => (
10✔
280
            <Stack direction="row" justifyContent="space-between">
8✔
281
              <StyledIssuesTextWrapper>
8✔
282
                <TruncatedText
8✔
283
                  text={data.errors?.[0]?.title || data.warnings?.[0]?.title}
8✔
284
                  maxCharacters={30}
8✔
285
                  wrapperSx={{ display: "inline" }}
8✔
286
                  labelSx={{ display: "inline" }}
8✔
287
                  disableInteractiveTooltip={false}
8✔
288
                />
289
                {data.issueCount > 1 ? (
8✔
290
                  <>
6✔
291
                    {" and "}
6✔
292
                    <StyledTooltip
6✔
293
                      title={TOOLTIP_TEXT.QUALITY_CONTROL.TABLE.CLICK_TO_VIEW_ALL_ISSUES}
6✔
294
                      placement="top"
6✔
295
                      disableInteractive
6✔
296
                      arrow
6✔
297
                    >
298
                      <StyledOthersText
6✔
299
                        onClick={() => handleOpenErrorDialog?.(data)}
6✔
300
                        data-testid="others-text"
6✔
301
                      >
302
                        other {data.issueCount - 1}
6✔
303
                      </StyledOthersText>
6✔
304
                    </StyledTooltip>
6✔
305
                  </>
6✔
306
                ) : null}
2✔
307
              </StyledIssuesTextWrapper>
8✔
308

309
              <StyledErrorDetailsButton
8✔
310
                onClick={() => handleOpenErrorDialog?.(data)}
8✔
311
                variant="text"
8✔
312
                disableRipple
8✔
313
                disableTouchRipple
8✔
314
                disableFocusRipple
8✔
315
              >
316
                See details.
317
              </StyledErrorDetailsButton>
8✔
318
            </Stack>
8✔
319
          )}
320
        </QCResultsContext.Consumer>
10✔
321
      ),
322
    sortDisabled: true,
1✔
323
  },
1✔
324
];
325

326
// CSV columns used for exporting table data
327
export const csvColumns = {
1✔
328
  "Batch ID": (d: QCResult) => d.displayID,
1✔
329
  "Node Type": (d: QCResult) => d.type,
1✔
330
  "Submitted Identifier": (d: QCResult) => d.submittedID,
1✔
331
  Severity: (d: QCResult) => d.severity,
1✔
332
  "Validated Date": (d: QCResult) => FormatDate(d?.validatedDate, "MM-DD-YYYY [at] hh:mm A", ""),
1✔
333
  "Issue Count": (d: QCResult) =>
1✔
NEW
334
    Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(d.issueCount || 0),
×
335
  "Issue(s)": (d: QCResult) => {
1✔
UNCOV
336
    const value = d.errors[0]?.description ?? d.warnings[0]?.description;
×
337

338
    // NOTE: The ErrorMessage descriptions contain non-standard double quotes
339
    // that don't render correctly in Excel. This replaces them with standard double quotes.
340
    return value.replaceAll(/[“”‟〞"]/g, `"`);
×
341
  },
×
342
};
1✔
343

344
export const aggregatedCSVColumns = {
1✔
345
  "Issue Type": (d: AggregatedQCResult) => d.title,
1✔
346
  Property: (d: AggregatedQCResult) => d.property,
1✔
347
  Value: (d: AggregatedQCResult) => d.value,
1✔
348
  Severity: (d: AggregatedQCResult) => d.severity,
1✔
349
  "Record Count": (d: AggregatedQCResult) =>
1✔
NEW
350
    Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(d.count || 0),
×
351
};
1✔
352

353
const QualityControl: FC = () => {
1✔
354
  const { enqueueSnackbar } = useSnackbar();
183✔
355
  const { data: submissionData } = useSubmissionContext();
183✔
356
  const {
183✔
357
    _id: submissionId,
183✔
358
    status: submissionStatus,
183✔
359
    metadataValidationStatus,
183✔
360
    fileValidationStatus,
183✔
361
  } = submissionData?.getSubmission || {};
183!
362

363
  const [loading, setLoading] = useState<boolean>(false);
183✔
364
  const [data, setData] = useState<RowData[]>([]);
183✔
365
  const [prevData, setPrevData] = useState<FetchListing<RowData>>(null);
183✔
366
  const [totalData, setTotalData] = useState(0);
183✔
367
  const [openErrorDialog, setOpenErrorDialog] = useState<boolean>(false);
183✔
368
  const [selectedRow, setSelectedRow] = useState<RowData | null>(null);
183✔
369
  const [isAggregated, setIsAggregated] = useState<boolean>(true);
183✔
370
  const [issueType, setIssueType] = useState<string | null>("All");
183✔
371
  const filtersRef: MutableRefObject<FilterForm> = useRef({
183✔
372
    issueType: "All",
183✔
373
    batchID: "All",
183✔
374
    nodeType: "All",
183✔
375
    severity: "All",
183✔
376
  });
183✔
377
  const tableRef = useRef<TableMethods>(null);
183✔
378

379
  const [submissionQCResults] = useLazyQuery<SubmissionQCResultsResp, SubmissionQCResultsInput>(
183✔
380
    SUBMISSION_QC_RESULTS,
183✔
381
    {
183✔
382
      context: { clientName: "backend" },
183✔
383
      fetchPolicy: "cache-and-network",
183✔
384
    }
183✔
385
  );
183✔
386

387
  const [aggregatedSubmissionQCResults] = useLazyQuery<
183✔
388
    AggregatedSubmissionQCResultsResp,
389
    AggregatedSubmissionQCResultsInput
390
  >(AGGREGATED_SUBMISSION_QC_RESULTS, {
183✔
391
    context: { clientName: "backend" },
183✔
392
    fetchPolicy: "cache-and-network",
183✔
393
  });
183✔
394

395
  const {
183✔
396
    data: pendingPVs,
183✔
397
    refetch: refetchPendingPVs,
183✔
398
    updateQuery: updatePendingPVs,
183✔
399
  } = useQuery<GetPendingPVsResponse, GetPendingPVsInput>(GET_PENDING_PVS, {
183✔
400
    variables: { submissionID: submissionId },
183✔
401
    context: { clientName: "backend" },
183✔
402
    fetchPolicy: "cache-and-network",
183✔
403
    notifyOnNetworkStatusChange: true,
183✔
404
    skip: !submissionId,
183✔
405
  });
183✔
406

407
  const handleFetchQCResults = async (fetchListing: FetchListing<QCResult>, force: boolean) => {
183✔
408
    const { first, offset, sortDirection, orderBy } = fetchListing || {};
23!
409
    if (!force && data?.length > 0 && isEqual(fetchListing, prevData)) {
23!
410
      return;
×
411
    }
×
412

413
    setPrevData(fetchListing);
23✔
414

415
    try {
23✔
416
      setLoading(true);
23✔
417

418
      const { data: d, error } = await submissionQCResults({
23✔
419
        variables: {
23✔
420
          id: submissionId,
23✔
421
          first,
23✔
422
          offset,
23✔
423
          sortDirection,
23✔
424
          orderBy,
23✔
425
          issueCode:
23✔
426
            !filtersRef.current.issueType || filtersRef.current.issueType === "All"
23✔
427
              ? undefined
23!
428
              : filtersRef.current.issueType,
×
429
          nodeTypes:
23✔
430
            !filtersRef.current.nodeType || filtersRef.current.nodeType === "All"
23✔
431
              ? undefined
22✔
432
              : [filtersRef.current.nodeType],
1✔
433
          batchIDs:
23✔
434
            !filtersRef.current.batchID || filtersRef.current.batchID === "All"
23✔
435
              ? undefined
21✔
436
              : [filtersRef.current.batchID],
2✔
437
          severities: filtersRef.current.severity || "All",
23!
438
        },
23✔
439
        context: { clientName: "backend" },
23✔
440
        fetchPolicy: "no-cache",
23✔
441
      });
23✔
442
      if (error || !d?.submissionQCResults) {
23✔
443
        throw new Error("Unable to retrieve submission quality control results.");
2✔
444
      }
2✔
445
      setData(d.submissionQCResults.results);
21✔
446
      setTotalData(d.submissionQCResults.total);
21✔
447
    } catch (err) {
23✔
448
      enqueueSnackbar(err?.toString(), { variant: "error" });
2✔
449
    } finally {
23✔
450
      setLoading(false);
23✔
451
    }
23✔
452
  };
23✔
453

454
  const handleFetchAggQCResults = async (
183✔
455
    fetchListing: FetchListing<AggregatedQCResult>,
38✔
456
    force: boolean
38✔
457
  ) => {
38✔
458
    const { first, offset, sortDirection, orderBy } = fetchListing || {};
38!
459

460
    if (!force && data?.length > 0 && isEqual(fetchListing, prevData)) {
38!
461
      return;
×
462
    }
×
463

464
    setPrevData(fetchListing);
38✔
465

466
    try {
38✔
467
      setLoading(true);
38✔
468

469
      const { data: d, error } = await aggregatedSubmissionQCResults({
38✔
470
        variables: {
38✔
471
          submissionID: submissionId,
38✔
472
          severity: filtersRef.current.severity?.toLowerCase() || "all",
38!
473
          first,
38✔
474
          offset,
38✔
475
          sortDirection,
38✔
476
          orderBy,
38✔
477
        },
38✔
478
        context: { clientName: "backend" },
38✔
479
        fetchPolicy: "no-cache",
38✔
480
      });
38✔
481
      if (error || !d?.aggregatedSubmissionQCResults) {
38✔
482
        throw new Error("Unable to retrieve submission aggregated quality control results.");
4✔
483
      }
4✔
484
      setData(d.aggregatedSubmissionQCResults.results);
34✔
485
      setTotalData(d.aggregatedSubmissionQCResults.total);
34✔
486
    } catch (err) {
38✔
487
      Logger.error(`QualityControl: ${err?.toString()}`);
4✔
488
      enqueueSnackbar(err?.toString(), { variant: "error" });
4✔
489
    } finally {
38✔
490
      setLoading(false);
38✔
491
    }
38✔
492
  };
38✔
493

494
  const handleFetchData = (fetchListing: FetchListing<RowData>, force: boolean) => {
183✔
495
    if (!submissionId || !filtersRef.current) {
63✔
496
      return;
2✔
497
    }
2✔
498

499
    isAggregated
61✔
500
      ? handleFetchAggQCResults(fetchListing, force)
38✔
501
      : handleFetchQCResults(fetchListing, force);
23✔
502
  };
63✔
503

504
  const handleOpenErrorDialog = (data: QCResult) => {
183✔
505
    setOpenErrorDialog(true);
1✔
506
    setSelectedRow(data);
1✔
507
  };
1✔
508

509
  const Actions = useMemo<React.ReactNode>(
183✔
510
    () => (
183✔
511
      <Stack direction="row" alignItems="center" gap="8px" marginRight="37px">
56✔
512
        <ExportValidationButton
56✔
513
          submission={submissionData?.getSubmission}
56✔
514
          fields={isAggregated ? aggregatedCSVColumns : csvColumns}
56✔
515
          isAggregated={isAggregated}
56✔
516
          disabled={totalData <= 0}
56✔
517
        />
518
      </Stack>
56✔
519
    ),
520
    [submissionData?.getSubmission, totalData, isAggregated]
183✔
521
  );
183✔
522

523
  const handleOnFiltersChange = (data: FilterForm) => {
183✔
524
    filtersRef.current = data;
36✔
525
    tableRef.current?.setPage(0, true);
36✔
526
  };
36✔
527

528
  const onSwitchToggle = () => {
183✔
529
    setIsAggregated((prev) => {
14✔
530
      const newVal = !prev;
14✔
531
      // Reset to 'All' when in Aggregated view
532
      if (newVal === true) {
14!
533
        setIssueType("All");
×
534
      }
×
535

536
      return newVal;
14✔
537
    });
14✔
538
  };
14✔
539

540
  const currentColumns = useMemo(
183✔
541
    () => (isAggregated ? aggregatedColumns : expandedColumns),
183✔
542
    [isAggregated]
183✔
543
  ) as Column<RowData>[];
183✔
544

545
  const handleExpandClick = (issue: AggregatedQCResult) => {
183✔
546
    if (!issue?.code) {
×
547
      Logger.error("QualityControl: Unable to expand invalid issue.");
×
548
      return;
×
549
    }
×
550

551
    setIssueType(issue?.code);
×
552
    setIsAggregated(false);
×
553
  };
×
554

555
  const providerValue = useMemo(
183✔
556
    () => ({
183✔
557
      handleOpenErrorDialog,
183✔
558
      handleExpandClick,
183✔
559
    }),
183✔
560
    [handleOpenErrorDialog, handleExpandClick]
183✔
561
  );
183✔
562

563
  const handleNewPVRequest = useCallback(
183✔
564
    (offendingProperty: string, offendingValue: string) => {
183✔
565
      updatePendingPVs((prev) => ({
×
566
        getPendingPVs: [
×
567
          ...(prev?.getPendingPVs || []),
×
568
          { id: `${Date.now()}`, offendingProperty, value: offendingValue },
×
569
        ],
570
      }));
×
571

572
      // NOTE: We refetch after small delay to allow cache to propagate
573
      setTimeout(refetchPendingPVs, 2000);
×
574
    },
×
575
    [updatePendingPVs, refetchPendingPVs]
183✔
576
  );
183✔
577

578
  const issueList = useMemo<ErrorDetailsIssue[]>(() => {
183✔
579
    if (!selectedRow || !("errors" in selectedRow) || !("warnings" in selectedRow)) {
34✔
580
      return [];
33✔
581
    }
33✔
582

583
    const allIssues: ErrorDetailsIssue[] = [];
1✔
584
    selectedRow.errors?.forEach((e) => {
34✔
585
      const issue: ErrorDetailsIssue = { severity: "error", message: e.description };
3✔
586

587
      if (e.code === ValidationErrorCodes.INVALID_PERMISSIBLE) {
3!
588
        const isDisabled = pendingPVs?.getPendingPVs?.some(
×
589
          (pv) => pv.offendingProperty === e.offendingProperty && pv.value === e.offendingValue
×
590
        );
×
591

592
        issue.action = (
×
593
          <StyledPvButtonWrapper>
×
594
            <PVRequestButton
×
595
              onSubmit={handleNewPVRequest}
×
596
              offendingProperty={e.offendingProperty}
×
597
              offendingValue={e.offendingValue}
×
598
              nodeName={selectedRow.type}
×
599
              disabled={isDisabled}
×
600
            />
601
          </StyledPvButtonWrapper>
×
602
        );
603
      }
×
604

605
      allIssues.push(issue);
3✔
606
    });
34✔
607
    selectedRow.warnings?.forEach((w) => {
34✔
608
      const issue: ErrorDetailsIssue = { severity: "warning", message: w.description };
×
609

610
      if (w.code === ValidationErrorCodes.UPDATING_DATA && submissionStatus !== "Completed") {
×
611
        issue.action = (
×
612
          <NodeComparison
×
613
            nodeType={selectedRow.type}
×
614
            submissionID={submissionId}
×
615
            submittedID={selectedRow.submittedID}
×
616
          />
617
        );
618
      }
×
619

620
      allIssues.push(issue);
×
621
    });
34✔
622

623
    return allIssues;
34✔
624
  }, [selectedRow, submissionStatus, pendingPVs, handleNewPVRequest]);
183✔
625

626
  useEffect(() => {
183✔
627
    tableRef.current?.refresh();
20✔
628
  }, [metadataValidationStatus, fileValidationStatus]);
183✔
629

630
  return (
183✔
631
    <>
183✔
632
      <QualityControlFilters
183✔
633
        onChange={handleOnFiltersChange}
183✔
634
        issueType={issueType}
183✔
635
        isAggregated={isAggregated}
183✔
636
      />
637

638
      <QCResultsContext.Provider value={providerValue}>
183✔
639
        <GenericTable
183✔
640
          ref={tableRef}
183✔
641
          columns={currentColumns}
183✔
642
          data={data || []}
183!
643
          total={totalData || 0}
183✔
644
          loading={loading}
183✔
645
          defaultRowsPerPage={20}
183✔
646
          defaultOrder="desc"
183✔
647
          position="both"
183✔
648
          CustomTableHeaderCell={StyledHeaderCell}
183✔
649
          CustomTableBodyCell={StyledTableCell}
183✔
650
          noContentText="No validation issues found. Either no validation has been conducted yet, or all issues have been resolved."
183✔
651
          setItemKey={(item, idx) => `${idx}_${"title" in item ? item?.title : item?.batchID}`}
183✔
652
          onFetchData={handleFetchData}
183✔
653
          AdditionalActions={{
183✔
654
            top: {
183✔
655
              before: (
183✔
656
                <DoubleLabelSwitch
183✔
657
                  leftLabel="Aggregated"
183✔
658
                  rightLabel="Expanded"
183✔
659
                  id="table-state-switch"
183✔
660
                  data-testid="table-view-switch"
183✔
661
                  checked={!isAggregated}
183✔
662
                  onChange={onSwitchToggle}
183✔
663
                  inputProps={{ "aria-label": "Aggregated or Expanded table view switch" }}
183✔
664
                />
665
              ),
666
              after: Actions,
183✔
667
            },
183✔
668
            bottom: {
183✔
669
              after: Actions,
183✔
670
            },
183✔
671
          }}
183✔
672
          containerProps={{ sx: { marginBottom: "8px" } }}
183✔
673
        />
674
      </QCResultsContext.Provider>
183✔
675
      {!isAggregated && (
183✔
676
        <ErrorDetailsDialog
117✔
677
          open={openErrorDialog}
117✔
678
          onClose={() => setOpenErrorDialog(false)}
117✔
679
          preHeader="Data Submission"
117✔
680
          header="Validation Issues"
117✔
681
          postHeader={`For ${titleCase((selectedRow as QCResult)?.type)}${
117✔
682
            (selectedRow as QCResult)?.type?.toLocaleLowerCase() !== "data file" ? " Node" : ""
117!
683
          } ID ${(selectedRow as QCResult)?.submittedID}`}
117✔
684
          issues={issueList}
117✔
685
        />
686
      )}
687
    </>
183✔
688
  );
689
};
183✔
690

691
export default React.memo(QualityControl);
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc