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

CBIIT / crdc-datahub-ui / 16006182009

01 Jul 2025 05:32PM UTC coverage: 62.703% (-8.6%) from 71.278%
16006182009

Pull #756

github

web-flow
Merge pull request #755 from CBIIT/revert-omb-date

revert: OMB expiration update
Pull Request #756: Sync 3.4.0 with 3.3.0

3560 of 6102 branches covered (58.34%)

Branch coverage included in aggregate %.

4920 of 7422 relevant lines covered (66.29%)

227.7 hits per line

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

87.72
/src/components/DataSubmissions/ValidationControls.tsx
1
import React, { FC, ReactElement, useEffect, useMemo, useRef, useState } from "react";
2
import { useMutation } from "@apollo/client";
3
import {
4
  FormControlLabel,
5
  RadioGroup,
6
  Stack,
7
  TooltipProps,
8
  Typography,
9
  styled,
10
} from "@mui/material";
11
import { LoadingButton } from "@mui/lab";
12
import { useSnackbar } from "notistack";
13
import { useAuthContext } from "../Contexts/AuthContext";
14
import StyledRadioButton from "../Questionnaire/StyledRadioButton";
15
import {
16
  VALIDATE_SUBMISSION,
17
  ValidateSubmissionInput,
18
  ValidateSubmissionResp,
19
} from "../../graphql";
20
import {
21
  getDefaultValidationTarget,
22
  getDefaultValidationType,
23
  getValidationTypes,
24
} from "../../utils";
25
import FlowWrapper from "./FlowWrapper";
26
import { CrossValidationButton } from "./CrossValidationButton";
27
import { ValidationStatus } from "./ValidationStatus";
28
import { useSubmissionContext } from "../Contexts/SubmissionContext";
29
import { TOOLTIP_TEXT } from "../../config/DashboardTooltips";
30
import StyledTooltip from "../StyledFormComponents/StyledTooltip";
31
import { hasPermission } from "../../config/AuthPermissions";
32

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

45
const StyledRow = styled(Stack)({
2✔
46
  fontFamily: "Nunito",
47
});
48

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

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

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

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

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

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

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

131
  const isValidating = useMemo<boolean>(
212✔
132
    () =>
133
      dataSubmission?.fileValidationStatus === "Validating" ||
80✔
134
      dataSubmission?.metadataValidationStatus === "Validating",
135
    [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]
136
  );
137
  const prevIsValidating = useRef<boolean>(isValidating);
212✔
138

139
  const canValidateMetadata: boolean = useMemo(() => {
212✔
140
    const hasPermission = ValidateMap[dataSubmission?.status]
80✔
141
      ? ValidateMap[dataSubmission?.status](user, dataSubmission)
142
      : null;
143
    if (!user?.role || !dataSubmission?.status || hasPermission === null) {
80✔
144
      return false;
14✔
145
    }
146
    if (hasPermission === false) {
66✔
147
      return false;
10✔
148
    }
149

150
    return dataSubmission?.metadataValidationStatus !== null;
56✔
151
  }, [user, dataSubmission]);
152

153
  const canValidateFiles: boolean = useMemo(() => {
212✔
154
    const hasPermission = ValidateMap[dataSubmission?.status]
80✔
155
      ? ValidateMap[dataSubmission?.status](user, dataSubmission)
156
      : null;
157
    if (!user?.role || !dataSubmission?.status || hasPermission === null) {
80✔
158
      return false;
14✔
159
    }
160
    if (hasPermission === false) {
66✔
161
      return false;
10✔
162
    }
163
    if (dataSubmission.intention === "Delete" || dataSubmission.dataType === "Metadata Only") {
56✔
164
      return false;
4✔
165
    }
166

167
    return dataSubmission?.fileValidationStatus !== null;
52✔
168
  }, [user, dataSubmission]);
169

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

178
  const handleValidateFiles = async () => {
212✔
179
    if (isValidating || !validationType || !uploadType) {
22!
180
      return;
×
181
    }
182
    if (!canValidateFiles && validationType === "file") {
22!
183
      return;
×
184
    }
185
    if (!canValidateMetadata && validationType === "metadata") {
22!
186
      return;
×
187
    }
188

189
    setIsLoading(true);
22✔
190

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

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

211
    setIsLoading(false);
22✔
212
  };
213

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

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

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

265
  useEffect(() => {
212✔
266
    const isValidating =
267
      dataSubmission?.fileValidationStatus === "Validating" ||
80✔
268
      dataSubmission?.metadataValidationStatus === "Validating";
269

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

276
    prevIsValidating.current = isValidating;
80✔
277
  }, [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]);
278

279
  useEffect(() => {
212✔
280
    if (typeof dataSubmission === "undefined") {
80!
281
      return;
×
282
    }
283
    if (validationType === null) {
80✔
284
      setValidationType(getDefaultValidationType(dataSubmission, user));
72✔
285
    }
286
    if (uploadType === null) {
80✔
287
      setUploadType(getDefaultValidationTarget(dataSubmission, user));
72✔
288
    }
289
  }, [dataSubmission, user]);
290

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

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