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

CBIIT / crdc-datahub-ui / 18789341118

24 Oct 2025 06:57PM UTC coverage: 78.178% (+15.5%) from 62.703%
18789341118

push

github

web-flow
Merge pull request #888 from CBIIT/3.4.0

3.4.0 Release

4977 of 5488 branches covered (90.69%)

Branch coverage included in aggregate %.

8210 of 9264 new or added lines in 257 files covered. (88.62%)

6307 existing lines in 120 files now uncovered.

30203 of 39512 relevant lines covered (76.44%)

213.36 hits per line

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

92.59
/src/components/DataSubmissions/ExportValidationButton.tsx
1
import { useLazyQuery } from "@apollo/client";
1✔
2
import { CloudDownload } from "@mui/icons-material";
1✔
3
import { IconButtonProps, IconButton, styled } from "@mui/material";
1✔
4
import dayjs from "dayjs";
1✔
5
import { useSnackbar } from "notistack";
1✔
6
import { unparse } from "papaparse";
1✔
7
import { useState } from "react";
1✔
8

9
import {
1✔
10
  AGGREGATED_SUBMISSION_QC_RESULTS,
11
  AggregatedSubmissionQCResultsInput,
12
  AggregatedSubmissionQCResultsResp,
13
  SUBMISSION_QC_RESULTS,
14
  SubmissionQCResultsResp,
15
} from "../../graphql";
16
import { downloadBlob, filterAlphaNumeric, Logger, unpackValidationSeverities } from "../../utils";
1✔
17
import StyledFormTooltip from "../StyledFormComponents/StyledTooltip";
1✔
18

19
export type Props = {
20
  /**
21
   * The full Data Submission object to export validation results for
22
   */
23
  submission: Submission;
24
  /**
25
   * The K:V pair of the fields that should be exported where
26
   * `key` is the column header and `value` is a function
27
   * that generates the exportable value
28
   *
29
   * @example { "Batch ID": (d) => d.displayID }
30
   */
31
  fields: Record<string, (row: QCResult | AggregatedQCResult) => string | number>;
32
  /**
33
   * Tells the component whether to export the "aggregated" or the "expanded" data.
34
   * @default false
35
   */
36
  isAggregated?: boolean;
37
} & IconButtonProps;
38

39
const StyledIconButton = styled(IconButton)({
1✔
40
  color: "#606060",
1✔
41
});
1✔
42

43
const StyledTooltip = styled(StyledFormTooltip)({
1✔
44
  "& .MuiTooltip-tooltip": {
1✔
45
    color: "#000000",
1✔
46
  },
1✔
47
});
1✔
48

49
/**
50
 * Provides the button and supporting functionality to export the validation results of a submission.
51
 *
52
 * @returns {React.FC} The export validation button.
53
 */
54
export const ExportValidationButton: React.FC<Props> = ({
1✔
55
  submission,
460✔
56
  fields,
460✔
57
  isAggregated = false,
460✔
58
  disabled,
460✔
59
  ...buttonProps
460✔
60
}: Props) => {
460✔
61
  const { enqueueSnackbar } = useSnackbar();
460✔
62
  const [loading, setLoading] = useState<boolean>(false);
460✔
63

64
  const [getSubmissionQCResults] = useLazyQuery<SubmissionQCResultsResp>(SUBMISSION_QC_RESULTS, {
460✔
65
    context: { clientName: "backend" },
460✔
66
    fetchPolicy: "no-cache",
460✔
67
  });
460✔
68

69
  const [getAggregatedSubmissionQCResults] = useLazyQuery<
460✔
70
    AggregatedSubmissionQCResultsResp,
71
    AggregatedSubmissionQCResultsInput
72
  >(AGGREGATED_SUBMISSION_QC_RESULTS, {
460✔
73
    context: { clientName: "backend" },
460✔
74
    fetchPolicy: "no-cache",
460✔
75
  });
460✔
76

77
  /**
78
   * Helper to generate CSV and trigger download.
79
   * This function:
80
   *  1) Optionally unpacks severities if not aggregated
81
   *  2) Uses the given `fields` to generate CSV rows
82
   *  3) Calls `downloadBlob` to save the CSV file
83
   *
84
   * @returns {void}
85
   */
86
  const createCSVAndDownload = (
460✔
87
    rows: (QCResult | AggregatedQCResult)[],
13✔
88
    filename: string,
13✔
89
    isAggregated: boolean
13✔
90
  ): void => {
13✔
91
    try {
13✔
92
      let finalRows = rows;
13✔
93

94
      if (!isAggregated) {
13✔
95
        finalRows = unpackValidationSeverities<QCResult>(rows as QCResult[]);
11✔
96
      }
11✔
97

98
      const fieldEntries = Object.entries(fields);
12✔
99
      const csvArray = finalRows.map((row) => {
12✔
100
        const csvRow: Record<string, string | number> = {};
24✔
101
        fieldEntries.forEach(([header, fn]) => {
24✔
102
          csvRow[header] = fn(row) ?? "";
50✔
103
        });
24✔
104
        return csvRow;
24✔
105
      });
12✔
106

107
      downloadBlob(unparse(csvArray), filename, "text/csv");
12✔
108
    } catch (err) {
13✔
109
      enqueueSnackbar(`Unable to export validation results. Error: ${err}`, { variant: "error" });
1✔
110
    }
1✔
111
  };
13✔
112

113
  /**
114
   *  Creates a file name by using the submission name, filtering by alpha-numeric characters,
115
   * then adding the date and time
116
   *
117
   * @returns {string} A formatted file name for the exported file
118
   */
119
  const createFileName = (): string => {
460✔
120
    const filteredName = filterAlphaNumeric(submission.name?.trim()?.replaceAll(" ", "-"), "-");
13✔
121
    return `${filteredName}-${dayjs().format("YYYY-MM-DDTHHmmss")}.csv`;
13✔
122
  };
13✔
123

124
  /**
125
   *  Will retrieve all of the aggregated submission QC results to
126
   * construct and download a CSV file
127
   *
128
   *
129
   * @returns {Promise<void>}
130
   */
131
  const handleAggregatedExportSetup = async (): Promise<void> => {
460✔
132
    setLoading(true);
5✔
133

134
    try {
5✔
135
      const { data, error } = await getAggregatedSubmissionQCResults({
5✔
136
        variables: {
5✔
137
          submissionID: submission?._id,
5✔
138
          partial: false,
5✔
139
          first: -1,
5✔
140
          orderBy: "title",
5✔
141
          sortDirection: "asc",
5✔
142
        },
5✔
143
      });
5✔
144

145
      if (error || !data?.aggregatedSubmissionQCResults?.results) {
5✔
146
        enqueueSnackbar("Unable to retrieve submission aggregated quality control results.", {
2✔
147
          variant: "error",
2✔
148
        });
2✔
149
        return;
2✔
150
      }
2✔
151

152
      if (!data.aggregatedSubmissionQCResults.results.length) {
5✔
153
        enqueueSnackbar("There are no aggregated validation results to export.", {
1✔
154
          variant: "error",
1✔
155
        });
1✔
156
        return;
1✔
157
      }
1✔
158

159
      createCSVAndDownload(data.aggregatedSubmissionQCResults.results, createFileName(), true);
2✔
160
    } catch (err) {
5!
161
      enqueueSnackbar(`Unable to export aggregated validation results. Error: ${err}`, {
×
UNCOV
162
        variant: "error",
×
UNCOV
163
      });
×
164
      Logger.error(
×
UNCOV
165
        `ExportValidationButton: Unable to export aggregated validation results. Error: ${err}`
×
UNCOV
166
      );
×
167
    } finally {
5✔
168
      setLoading(false);
5✔
169
    }
5✔
170
  };
5✔
171

172
  /**
173
   *  Will retrieve all of the expanded submission QC results to
174
   * construct and download a CSV file
175
   *
176
   *
177
   * @returns {Promise<void>}
178
   */
179
  const handleExpandedExportSetup = async () => {
460✔
180
    setLoading(true);
14✔
181

182
    try {
14✔
183
      const { data, error } = await getSubmissionQCResults({
14✔
184
        variables: {
14✔
185
          id: submission?._id,
14✔
186
          sortDirection: "asc",
14✔
187
          orderBy: "displayID",
14✔
188
          first: -1,
14✔
189
          offset: 0,
14✔
190
        },
14✔
191
      });
14✔
192

193
      if (error || !data?.submissionQCResults?.results) {
14✔
194
        enqueueSnackbar("Unable to retrieve submission quality control results.", {
2✔
195
          variant: "error",
2✔
196
        });
2✔
197
        return;
2✔
198
      }
2✔
199

200
      if (!data.submissionQCResults.results.length) {
14✔
201
        enqueueSnackbar("There are no validation results to export.", { variant: "error" });
1✔
202
        return;
1✔
203
      }
1✔
204

205
      createCSVAndDownload(data.submissionQCResults.results, createFileName(), false);
11✔
206
    } catch (err) {
14!
207
      enqueueSnackbar(`Unable to export expanded validation results. Error: ${err}`, {
×
UNCOV
208
        variant: "error",
×
UNCOV
209
      });
×
210
      Logger.error(
×
UNCOV
211
        `ExportValidationButton: Unable to export expanded validation results. Error: ${err}`
×
UNCOV
212
      );
×
213
    } finally {
14✔
214
      setLoading(false);
14✔
215
    }
14✔
216
  };
14✔
217

218
  /**
219
   * Click handler that triggers the setup
220
   * for aggregated or expanded CSV file exporting
221
   */
222
  const handleClick = async () => {
460✔
223
    if (isAggregated) {
19✔
224
      handleAggregatedExportSetup();
5✔
225
      return;
5✔
226
    }
5✔
227

228
    handleExpandedExportSetup();
14✔
229
  };
19✔
230

231
  return (
460✔
232
    <StyledTooltip
460✔
233
      title={
460✔
234
        <span>
460✔
235
          Export all validation issues for this data <br />
460✔
236
          submission to a CSV file
237
        </span>
460✔
238
      }
239
      placement="top"
460✔
240
      data-testid="export-validation-tooltip"
460✔
241
    >
242
      <span>
460✔
243
        <StyledIconButton
460✔
244
          onClick={handleClick}
460✔
245
          disabled={loading || disabled}
460✔
246
          data-testid="export-validation-button"
460✔
247
          aria-label="Export validation results"
460✔
248
          {...buttonProps}
460✔
249
        >
250
          <CloudDownload />
460✔
251
        </StyledIconButton>
460✔
252
      </span>
460✔
253
    </StyledTooltip>
460✔
254
  );
255
};
460✔
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