• 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

91.76
/src/components/DataSubmissions/ValidationControls.tsx
1
import { useMutation } from "@apollo/client";
1✔
2
import { LoadingButton } from "@mui/lab";
1✔
3
import {
1✔
4
  FormControlLabel,
5
  RadioGroup,
6
  Stack,
7
  TooltipProps,
8
  Typography,
9
  styled,
10
} from "@mui/material";
11
import { useSnackbar } from "notistack";
1✔
12
import React, { FC, ReactElement, useEffect, useMemo, useRef, useState } from "react";
1✔
13

14
import { hasPermission } from "../../config/AuthPermissions";
1✔
15
import { TOOLTIP_TEXT } from "../../config/DashboardTooltips";
1✔
16
import {
1✔
17
  VALIDATE_SUBMISSION,
18
  ValidateSubmissionInput,
19
  ValidateSubmissionResp,
20
} from "../../graphql";
21
import {
1✔
22
  getDefaultValidationTarget,
23
  getDefaultValidationType,
24
  getValidationTypes,
25
} from "../../utils";
26
import { useAuthContext } from "../Contexts/AuthContext";
1✔
27
import { useSubmissionContext } from "../Contexts/SubmissionContext";
1✔
28
import StyledRadioButton from "../Questionnaire/StyledRadioButton";
1✔
29
import StyledTooltip from "../StyledFormComponents/StyledTooltip";
1✔
30

31
import { CrossValidationButton } from "./CrossValidationButton";
1✔
32
import FlowWrapper from "./FlowWrapper";
1✔
33
import { ValidationStatus } from "./ValidationStatus";
1✔
34

35
const StyledValidateButton = styled(LoadingButton)({
1✔
36
  padding: "10px",
1✔
37
  fontSize: "16px",
1✔
38
  fontStyle: "normal",
1✔
39
  lineHeight: "24px",
1✔
40
  letterSpacing: "0.32px",
1✔
41
  "&.MuiButtonBase-root": {
1✔
42
    marginLeft: "auto",
1✔
43
    minWidth: "137px",
1✔
44
  },
1✔
45
});
1✔
46

47
const StyledRow = styled(Stack)({
1✔
48
  fontFamily: "Nunito",
1✔
49
});
1✔
50

51
const StyledRowTitle = styled(Typography)({
1✔
52
  fontWeight: 700,
1✔
53
  fontSize: "16px",
1✔
54
  color: "#083A50",
1✔
55
  minWidth: "170px",
1✔
56
});
1✔
57

58
const StyledRowContent = styled(Stack)({
1✔
59
  display: "flex",
1✔
60
  flexDirection: "row",
1✔
61
  alignItems: "center",
1✔
62
  width: "650px",
1✔
63
});
1✔
64

65
const StyledRadioControl = styled(FormControlLabel)({
1✔
66
  fontFamily: "Nunito",
1✔
67
  fontSize: "16px",
1✔
68
  fontWeight: "500",
1✔
69
  lineHeight: "20px",
1✔
70
  textAlign: "left",
1✔
71
  color: "#083A50",
1✔
72
  minWidth: "230px",
1✔
73
  "&:last-child": {
1✔
74
    marginRight: "0px",
1✔
75
    minWidth: "unset",
1✔
76
  },
1✔
77
});
1✔
78

79
/**
80
 * A map from Submission Status to the user roles that can validate the submission for that status.
81
 *
82
 * @note All of the permission logic really should be refactored into a hook or otherwise.
83
 */
84
const ValidateMap: Partial<
1✔
85
  Record<Submission["status"], (user: User, submission: Submission) => boolean>
86
> = {
1✔
87
  "In Progress": (user: User, submission: Submission) =>
1✔
88
    hasPermission(user, "data_submission", "create", submission) ||
50✔
89
    hasPermission(user, "data_submission", "review", submission),
2✔
90
  Withdrawn: (user: User, submission: Submission) =>
1✔
91
    hasPermission(user, "data_submission", "create", submission) ||
×
UNCOV
92
    hasPermission(user, "data_submission", "review", submission),
×
93
  Rejected: (user: User, submission: Submission) =>
1✔
94
    hasPermission(user, "data_submission", "create", submission) ||
×
UNCOV
95
    hasPermission(user, "data_submission", "review", submission),
×
96
  Submitted: (user: User, submission: Submission) =>
1✔
97
    hasPermission(user, "data_submission", "review", submission),
16✔
98
};
1✔
99

100
const CustomTooltip = (props: TooltipProps) => (
1✔
101
  <StyledTooltip
580✔
102
    {...props}
580✔
103
    slotProps={{
580✔
104
      popper: {
580✔
105
        modifiers: [
580✔
106
          {
580✔
107
            name: "offset",
580✔
108
            options: {
580✔
109
              offset: [0, -14],
580✔
110
            },
580✔
111
          },
580✔
112
        ],
113
      },
580✔
114
    }}
580✔
115
  />
116
);
117

118
/**
119
 * Provides the UI for validating a data submission's assets.
120
 *
121
 * @returns {React.FC}
122
 */
123
const ValidationControls: FC = () => {
1✔
124
  const { user } = useAuthContext();
117✔
125
  const { enqueueSnackbar } = useSnackbar();
117✔
126
  const { data, updateQuery, refetch } = useSubmissionContext();
117✔
127
  const { getSubmission: dataSubmission } = data || {};
117!
128

129
  const [validationType, setValidationType] = useState<ValidationType | "All">(null);
117✔
130
  const [uploadType, setUploadType] = useState<ValidationTarget>(null);
117✔
131
  const [isLoading, setIsLoading] = useState<boolean>(false);
117✔
132

133
  const isValidating = useMemo<boolean>(
117✔
134
    () =>
117✔
135
      dataSubmission?.fileValidationStatus === "Validating" ||
40✔
136
      dataSubmission?.metadataValidationStatus === "Validating",
37✔
137
    [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]
117✔
138
  );
117✔
139
  const prevIsValidating = useRef<boolean>(isValidating);
117✔
140

141
  const canValidateMetadata: boolean = useMemo(() => {
117✔
142
    const hasPermission = ValidateMap[dataSubmission?.status]
40✔
143
      ? ValidateMap[dataSubmission?.status](user, dataSubmission)
33✔
144
      : null;
7✔
145
    if (!user?.role || !dataSubmission?.status || hasPermission === null) {
40✔
146
      return false;
7✔
147
    }
7✔
148
    if (hasPermission === false) {
40✔
149
      return false;
5✔
150
    }
5✔
151

152
    return dataSubmission?.metadataValidationStatus !== null;
40✔
153
  }, [user, dataSubmission]);
117✔
154

155
  const canValidateFiles: boolean = useMemo(() => {
117✔
156
    const hasPermission = ValidateMap[dataSubmission?.status]
40✔
157
      ? ValidateMap[dataSubmission?.status](user, dataSubmission)
33✔
158
      : null;
7✔
159
    if (!user?.role || !dataSubmission?.status || hasPermission === null) {
40✔
160
      return false;
7✔
161
    }
7✔
162
    if (hasPermission === false) {
40✔
163
      return false;
5✔
164
    }
5✔
165
    if (dataSubmission.intention === "Delete" || dataSubmission.dataType === "Metadata Only") {
40✔
166
      return false;
2✔
167
    }
2✔
168

169
    return dataSubmission?.fileValidationStatus !== null;
40✔
170
  }, [user, dataSubmission]);
117✔
171

172
  const [validateSubmission] = useMutation<ValidateSubmissionResp, ValidateSubmissionInput>(
117✔
173
    VALIDATE_SUBMISSION,
117✔
174
    {
117✔
175
      context: { clientName: "backend" },
117✔
176
      fetchPolicy: "no-cache",
117✔
177
    }
117✔
178
  );
117✔
179

180
  const handleValidateFiles = async () => {
117✔
181
    if (isValidating || !validationType || !uploadType) {
11!
182
      return;
×
UNCOV
183
    }
×
184
    if (!canValidateFiles && validationType === "file") {
11!
185
      return;
×
UNCOV
186
    }
×
187
    if (!canValidateMetadata && validationType === "metadata") {
11!
188
      return;
×
UNCOV
189
    }
×
190

191
    setIsLoading(true);
11✔
192

193
    const { data, errors } = await validateSubmission({
11✔
194
      variables: {
11✔
195
        _id: dataSubmission?._id,
11✔
196
        types: getValidationTypes(validationType),
11✔
197
        scope: uploadType,
11✔
198
      },
11✔
199
    }).catch((e) => ({ errors: e?.message, data: null }));
11✔
200

201
    if (errors || !data?.validateSubmission?.success) {
11✔
202
      enqueueSnackbar("Unable to initiate validation process.", {
2✔
203
        variant: "error",
2✔
204
      });
2✔
205
    } else {
11✔
206
      enqueueSnackbar(
9✔
207
        "Validation process is starting; this may take some time. Please wait before initiating another validation.",
9✔
208
        { variant: "success" }
9✔
209
      );
9✔
210
      handleOnValidate();
9✔
211
    }
9✔
212

213
    setIsLoading(false);
11✔
214
  };
11✔
215

216
  const handleOnValidate = () => {
117✔
217
    // NOTE: This forces the UI to rerender with the new statuses immediately
218
    const types = getValidationTypes(validationType);
9✔
219
    updateQuery((prev) => ({
9✔
UNCOV
220
      ...prev,
×
UNCOV
221
      getSubmission: {
×
UNCOV
222
        ...prev.getSubmission,
×
UNCOV
223
        fileValidationStatus: types?.includes("file")
×
UNCOV
224
          ? "Validating"
×
UNCOV
225
          : prev?.getSubmission?.fileValidationStatus,
×
UNCOV
226
        metadataValidationStatus: types?.includes("metadata")
×
UNCOV
227
          ? "Validating"
×
UNCOV
228
          : prev?.getSubmission?.metadataValidationStatus,
×
UNCOV
229
        validationStarted: new Date().toISOString(),
×
UNCOV
230
        validationEnded: null,
×
UNCOV
231
        validationType: types,
×
UNCOV
232
        validationScope: uploadType,
×
UNCOV
233
      },
×
234
    }));
9✔
235

236
    // Kick off polling to check for validation status change
237
    // NOTE: We're waiting 1000ms to allow the cache to update
238
    setTimeout(refetch, 1000);
9✔
239
  };
9✔
240

241
  const Actions: ReactElement = useMemo(
117✔
242
    () => (
117✔
243
      <>
117✔
244
        <StyledValidateButton
117✔
245
          variant="contained"
117✔
246
          color="info"
117✔
247
          disabled={(!canValidateFiles && !canValidateMetadata) || isValidating}
117✔
248
          loading={isLoading}
117✔
249
          onClick={handleValidateFiles}
117✔
250
          data-testid="validate-controls-validate-button"
117✔
251
        >
252
          {isValidating ? "Validating..." : "Validate"}
117✔
253
        </StyledValidateButton>
117✔
254
        <CrossValidationButton submission={dataSubmission} variant="contained" color="info" />
117✔
255
      </>
117✔
256
    ),
257
    [
117✔
258
      handleValidateFiles,
117✔
259
      dataSubmission,
117✔
260
      canValidateFiles,
117✔
261
      canValidateMetadata,
117✔
262
      isValidating,
117✔
263
      isLoading,
117✔
264
    ]
265
  );
117✔
266

267
  useEffect(() => {
117✔
268
    const isValidating =
40✔
269
      dataSubmission?.fileValidationStatus === "Validating" ||
40✔
270
      dataSubmission?.metadataValidationStatus === "Validating";
37✔
271

272
    // Reset the validation type and target only if the validation process finished
273
    if (!isValidating && prevIsValidating.current === true) {
40✔
274
      setValidationType(getDefaultValidationType(dataSubmission, user));
2✔
275
      setUploadType(getDefaultValidationTarget(dataSubmission, user));
2✔
276
    }
2✔
277

278
    prevIsValidating.current = isValidating;
40✔
279
  }, [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]);
117✔
280

281
  useEffect(() => {
117✔
282
    if (typeof dataSubmission === "undefined") {
40!
283
      return;
×
UNCOV
284
    }
×
285
    if (validationType === null) {
40✔
286
      setValidationType(getDefaultValidationType(dataSubmission, user));
36✔
287
    }
36✔
288
    if (uploadType === null) {
40✔
289
      setUploadType(getDefaultValidationTarget(dataSubmission, user));
36✔
290
    }
36✔
291
  }, [dataSubmission, user]);
117✔
292

293
  return (
117✔
294
    <FlowWrapper
117✔
295
      index={3}
117✔
296
      titleContainerSx={{ marginBottom: "4px", columnGap: "12px" }}
117✔
297
      title="Validate Data"
117✔
298
      titleAdornment={<ValidationStatus />}
117✔
299
      actions={Actions}
117✔
300
      last
117✔
301
    >
302
      <>
117✔
303
        <StyledRow direction="row" alignItems="center" sx={{ marginBottom: "-5px" }}>
117✔
304
          <StyledRowTitle>Validation Type:</StyledRowTitle>
117✔
305
          <StyledRowContent>
117✔
306
            <RadioGroup
117✔
307
              value={validationType}
117✔
308
              onChange={(e, val: ValidationType) => setValidationType(val)}
117✔
309
              data-testid="validate-controls-validation-type"
117✔
310
              row
117✔
311
            >
312
              <CustomTooltip
117✔
313
                placement="bottom"
117✔
314
                title={TOOLTIP_TEXT.VALIDATION_CONTROLS.VALIDATION_TYPE.VALIDATE_METADATA}
117✔
315
                open={undefined} // will use hoverListener to open
117✔
316
                disableHoverListener={false}
117✔
317
              >
318
                <StyledRadioControl
117✔
319
                  value="metadata"
117✔
320
                  control={<StyledRadioButton readOnly={false} />}
117✔
321
                  label="Validate Metadata"
117✔
322
                  disabled={!canValidateMetadata}
117✔
323
                />
324
              </CustomTooltip>
117✔
325
              <CustomTooltip
117✔
326
                placement="bottom"
117✔
327
                title={TOOLTIP_TEXT.VALIDATION_CONTROLS.VALIDATION_TYPE.VALIDATE_DATA_FILES}
117✔
328
                open={undefined} // will use hoverListener to open
117✔
329
                disableHoverListener={false}
117✔
330
              >
331
                <StyledRadioControl
117✔
332
                  value="file"
117✔
333
                  control={<StyledRadioButton readOnly={false} />}
117✔
334
                  label="Validate Data Files"
117✔
335
                  disabled={!canValidateFiles}
117✔
336
                />
337
              </CustomTooltip>
117✔
338
              <CustomTooltip
117✔
339
                placement="bottom"
117✔
340
                title={TOOLTIP_TEXT.VALIDATION_CONTROLS.VALIDATION_TYPE.VALIDATE_BOTH}
117✔
341
                open={undefined} // will use hoverListener to open
117✔
342
                disableHoverListener={false}
117✔
343
              >
344
                <StyledRadioControl
117✔
345
                  value="All"
117✔
346
                  control={<StyledRadioButton readOnly={false} />}
117✔
347
                  label="Both"
117✔
348
                  disabled={!canValidateFiles || !canValidateMetadata}
117✔
349
                />
350
              </CustomTooltip>
117✔
351
            </RadioGroup>
117✔
352
          </StyledRowContent>
117✔
353
        </StyledRow>
117✔
354
        <StyledRow direction="row" alignItems="center" sx={{ marginTop: "-5px" }}>
117✔
355
          <StyledRowTitle>Validation Target:</StyledRowTitle>
117✔
356
          <StyledRowContent>
117✔
357
            <RadioGroup
117✔
358
              value={uploadType}
117✔
359
              onChange={(event, val: ValidationTarget) => setUploadType(val)}
117✔
360
              data-testid="validate-controls-validation-target"
117✔
361
              row
117✔
362
            >
363
              <CustomTooltip
117✔
364
                placement="bottom"
117✔
365
                title={TOOLTIP_TEXT.VALIDATION_CONTROLS.VALIDATION_TARGET.NEW_UPLOADED_DATA}
117✔
366
                open={undefined} // will use hoverListener to open
117✔
367
                disableHoverListener={false}
117✔
368
              >
369
                <StyledRadioControl
117✔
370
                  value="New"
117✔
371
                  control={<StyledRadioButton readOnly={false} />}
117✔
372
                  label="New Uploaded Data"
117✔
373
                  disabled={
117✔
374
                    (!canValidateFiles && !canValidateMetadata) ||
117✔
375
                    // NOTE: No new data to validate if the submission is already submitted
376
                    dataSubmission?.status === "Submitted"
91✔
377
                  }
117✔
378
                />
379
              </CustomTooltip>
117✔
380
              <CustomTooltip
117✔
381
                placement="bottom"
117✔
382
                title={TOOLTIP_TEXT.VALIDATION_CONTROLS.VALIDATION_TARGET.ALL_UPLOADED_DATA}
117✔
383
                open={undefined} // will use hoverListener to open
117✔
384
                disableHoverListener={false}
117✔
385
              >
386
                <StyledRadioControl
117✔
387
                  value="All"
117✔
388
                  control={<StyledRadioButton readOnly={false} />}
117✔
389
                  label="All Uploaded Data"
117✔
390
                  disabled={!canValidateFiles && !canValidateMetadata}
117✔
391
                />
392
              </CustomTooltip>
117✔
393
            </RadioGroup>
117✔
394
          </StyledRowContent>
117✔
395
        </StyledRow>
117✔
396
      </>
117✔
397
    </FlowWrapper>
117✔
398
  );
399
};
117✔
400

401
export default React.memo(ValidationControls);
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