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

CBIIT / crdc-datahub-ui / 25393067319

05 May 2026 05:54PM UTC coverage: 84.707% (+0.002%) from 84.705%
25393067319

Pull #992

github

web-flow
Merge pull request #991 from CBIIT/CRDCDH-3729

CRDCDH-3729 Fix template import running double migrations
Pull Request #992: Sync 3.7.0 with 3.6.0

6011 of 6621 branches covered (90.79%)

Branch coverage included in aggregate %.

4 of 8 new or added lines in 2 files covered. (50.0%)

64 existing lines in 1 file now uncovered.

35875 of 42827 relevant lines covered (83.77%)

239.59 hits per line

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

62.4
/src/content/questionnaire/FormView.tsx
1
import { LoadingButton } from "@mui/lab";
1✔
2
import {
1✔
3
  Checkbox,
4
  CheckboxProps,
5
  Container,
6
  Divider,
7
  FormControlLabel,
8
  Stack,
9
  styled,
10
} from "@mui/material";
11
import { isEqual, cloneDeep } from "lodash";
1✔
12
import { useSnackbar } from "notistack";
1✔
13
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
1✔
14
import { useNavigate, useBlocker, Blocker, Navigate, useLocation } from "react-router-dom";
1✔
15

16
import bannerPng from "../../assets/banner/submission_banner.png";
1✔
17
import CheckboxCheckedIconSvg from "../../assets/icons/checkbox_checked.svg?url";
1✔
18
import ChevronLeft from "../../assets/icons/chevron_left.svg?react";
1✔
19
import ChevronRight from "../../assets/icons/chevron_right.svg?react";
1✔
20
import CancelApplicationButton from "../../components/CancelApplicationButton";
1✔
21
import { Status as AuthStatus, useAuthContext } from "../../components/Contexts/AuthContext";
1✔
22
import { Status as FormStatus, useFormContext } from "../../components/Contexts/FormContext";
1✔
23
import PageBanner from "../../components/PageBanner";
1✔
24
import ProgressBar from "../../components/ProgressBar/ProgressBar";
1✔
25
import ReviewFormDialog from "../../components/Questionnaire/ReviewFormDialog";
1✔
26
import SubmitFormDialog from "../../components/Questionnaire/SubmitFormDialog";
1✔
27
import UnsavedChangesDialog from "../../components/Questionnaire/UnsavedChangesDialog";
1✔
28
import StatusBar from "../../components/StatusBar/StatusBar";
1✔
29
import StyledFormTooltip from "../../components/StyledFormComponents/StyledTooltip";
1✔
30
import SuspenseLoader from "../../components/SuspenseLoader";
1✔
31
import { hasPermission } from "../../config/AuthPermissions";
1✔
32
import map, { InitialSections } from "../../config/SectionConfig";
1✔
33
import useFormMode from "../../hooks/useFormMode";
1✔
34
import usePageTitle from "../../hooks/usePageTitle";
1✔
35
import { determineSectionStatus, Logger, sectionHasData } from "../../utils";
1✔
36

37
import Section from "./sections";
1✔
38

39
const StyledContainer = styled(Container)(() => ({
1✔
40
  "&.MuiContainer-root": {
29✔
41
    padding: 0,
29✔
42
    minHeight: "300px",
29✔
43
    scrollMarginTop: "-60px",
29✔
44
  },
29✔
45
}));
1✔
46

47
const StyledSidebar = styled(Stack)({
1✔
48
  position: "sticky",
1✔
49
  top: 0,
1✔
50
  marginTop: "90px",
1✔
51
});
1✔
52

53
const StyledDivider = styled(Divider)({
1✔
54
  height: "520px",
1✔
55
  width: "1px",
1✔
56
  borderRightWidth: "2px",
1✔
57
  borderRightColor: "#E8EAEE9",
1✔
58
  margin: "0 23px",
1✔
59
});
1✔
60

61
const StyledContentWrapper = styled(Stack)({
1✔
62
  paddingBottom: "75px",
1✔
63
});
1✔
64

65
const StyledContent = styled(Stack)({
1✔
66
  width: "100%",
1✔
67
  maxWidth: "980px",
1✔
68
  marginLeft: "41px",
1✔
69
});
1✔
70

71
const StyledControls = styled(Stack)({
1✔
72
  color: "#FFFFFF",
1✔
73
  marginTop: "15px !important",
1✔
74
  "& .MuiButton-root": {
1✔
75
    margin: "0 6px",
1✔
76
    padding: "10px 28px 10px !important",
1✔
77
    minWidth: "128px",
1✔
78
    fontSize: "16px",
1✔
79
    fontFamily: "'Nunito', 'Rubik', sans-serif",
1✔
80
    letterSpacing: "0.32px",
1✔
81
    lineHeight: "24px",
1✔
82
    borderRadius: "8px",
1✔
83
    textTransform: "none",
1✔
84
  },
1✔
85
  "& a": {
1✔
86
    color: "inherit",
1✔
87
    textDecoration: "none",
1✔
88
  },
1✔
89
  "& .MuiButton-startIcon": {
1✔
90
    marginRight: "20px",
1✔
91
    marginLeft: 0,
1✔
92
  },
1✔
93
  "& .MuiButton-endIcon": {
1✔
94
    marginRight: 0,
1✔
95
    marginLeft: "20px",
1✔
96
  },
1✔
97
  "& .MuiSvgIcon-root": {
1✔
98
    fontSize: "20px",
1✔
99
  },
1✔
100
});
1✔
101

102
const StyledLoadingButton = styled(LoadingButton)({
1✔
103
  "&.MuiButton-root": {
1✔
104
    display: "flex",
1✔
105
    justifyContent: "center",
1✔
106
    alignItems: "center",
1✔
107
    minWidth: "128px",
1✔
108
    padding: "10px",
1✔
109
    fontFamily: "'Nunito', 'Rubik', sans-serif",
1✔
110
    fontSize: "16px",
1✔
111
    fontStyle: "normal",
1✔
112
    lineHeight: "24px",
1✔
113
    letterSpacing: "0.32px",
1✔
114
    "& .MuiSvgIcon-root": {
1✔
115
      fontSize: "20px",
1✔
116
    },
1✔
117
  },
1✔
118
});
1✔
119

120
const StyledExtendedLoadingButton = styled(StyledLoadingButton)({
1✔
121
  "&.MuiButton-root": {
1✔
122
    minWidth: "137px",
1✔
123
  },
1✔
124
});
1✔
125

126
const StyledTooltip = styled(StyledFormTooltip)({
1✔
127
  margin: "0 !important",
1✔
128
  "& .MuiTooltip-tooltip": {
1✔
129
    color: "#000000",
1✔
130
  },
1✔
131
});
1✔
132

133
const UncheckedIcon = styled("div")<{ readOnly?: boolean }>(({ readOnly }) => ({
1✔
134
  outline: "2px solid #1D91AB",
5✔
135
  outlineOffset: -2,
5✔
136
  width: "24px",
5✔
137
  height: "24px",
5✔
138
  backgroundColor: readOnly ? "#E5EEF4" : "initial",
5!
139
  color: "#083A50",
5✔
140
  cursor: readOnly ? "not-allowed" : "pointer",
5!
141
}));
1✔
142

143
const CheckedIcon = styled("div")<{ readOnly?: boolean }>(({ readOnly }) => ({
1✔
144
  backgroundImage: `url("${CheckboxCheckedIconSvg}")`,
3✔
145
  backgroundSize: "auto",
3✔
146
  backgroundRepeat: "no-repeat",
3✔
147
  width: "24px",
3✔
148
  height: "24px",
3✔
149
  backgroundColor: readOnly ? "#E5EEF4" : "initial",
3!
150
  color: "#1D91AB",
3✔
151
  cursor: readOnly ? "not-allowed" : "pointer",
3!
152
}));
1✔
153

154
const StyledCheckbox = styled(Checkbox)({
1✔
155
  "&.MuiCheckbox-root": {
1✔
156
    padding: "10px",
1✔
157
  },
1✔
158
  "& .MuiSvgIcon-root": {
1✔
159
    fontSize: "24px",
1✔
160
  },
1✔
161
  "&.Mui-disabled": {
1✔
162
    cursor: "not-allowed",
1✔
163
  },
1✔
164
});
1✔
165

166
const validateSection = (section: string): section is SectionKey =>
1✔
167
  typeof map[section] !== "undefined";
39✔
168

169
export type SaveForm =
170
  | { status: "success"; id: string }
171
  | { status: "failed"; errorMessage: string };
172

173
type Props = {
174
  section?: string;
175
};
176

177
/**
178
 * Intake Form View Component
179
 *
180
 * @param {Props} props
181
 * @returns {JSX.Element}
182
 */
183
const FormView: FC<Props> = ({ section }: Props) => {
1✔
184
  const navigate = useNavigate();
29✔
185
  const location = useLocation();
29✔
186

187
  const { enqueueSnackbar } = useSnackbar();
29✔
188
  const {
29✔
189
    status,
29✔
190
    data,
29✔
191
    setData,
29✔
192
    submitData,
29✔
193
    approveForm,
29✔
194
    inquireForm,
29✔
195
    rejectForm,
29✔
196
    reopenForm,
29✔
197
    error,
29✔
198
  } = useFormContext();
29✔
199
  const { user, status: authStatus } = useAuthContext();
29✔
200
  const { formMode, readOnlyInputs } = useFormMode();
29✔
201

202
  const [activeSection, setActiveSection] = useState<SectionKey>(
29✔
203
    validateSection(section) ? section : "A"
29!
204
  );
29✔
205
  const [blockedNavigate, setBlockedNavigate] = useState<boolean>(false);
29✔
206
  const [openSubmitDialog, setOpenSubmitDialog] = useState<boolean>(false);
29✔
207
  const [openApproveDialog, setOpenApproveDialog] = useState<boolean>(false);
29✔
208
  const [openInquireDialog, setOpenInquireDialog] = useState<boolean>(false);
29✔
209
  const [openRejectDialog, setOpenRejectDialog] = useState<boolean>(false);
29✔
210
  const [pendingModelChange, setPendingModelChange] = useState<boolean>(false);
29✔
211
  const [pendingImageDeIdentification, setPendingImageDeIdentification] = useState<boolean>(false);
29✔
212
  const [allSectionsComplete, setAllSectionsComplete] = useState<boolean>(false);
29✔
213

214
  const sectionKeys = Object.keys(map);
29✔
215
  const sectionIndex = sectionKeys.indexOf(activeSection);
29✔
216
  const prevSection = sectionKeys[sectionIndex - 1]
29✔
217
    ? `/submission-request/${data?._id}/${sectionKeys[sectionIndex - 1]}`
21✔
218
    : null;
8✔
219
  const nextSection = sectionKeys[sectionIndex + 1]
29✔
220
    ? `/submission-request/${data?._id}/${sectionKeys[sectionIndex + 1]}`
8✔
221
    : null;
21✔
222
  const isSectionD = activeSection === "D";
29✔
223
  const formContentRef = useRef(null);
29✔
224
  const lastSectionRef = useRef(null);
29✔
225
  const hasReopenedFormRef = useRef(false);
29✔
226
  const bypassBlockerRef = useRef<boolean>(false);
29✔
227
  const previousIDRef = useRef<string | null>(null);
29✔
228
  const shouldShowToolTip = isSectionD && !allSectionsComplete;
29!
229

230
  const refs: FormSectionProps["refs"] = {
29✔
231
    getFormObjectRef: useRef<(() => FormObject) | null>(null),
29✔
232
  };
29✔
233

234
  usePageTitle(`Submission Request${data?._id && data?._id !== "new" ? ` - ${data._id}` : ""}`);
29✔
235

236
  /**
237
   * Determines if the form has unsaved changes.
238
   *
239
   * @returns {boolean} true if the form has unsaved changes, false otherwise
240
   */
241
  const isDirty = (): boolean => {
29✔
242
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
243

244
    return ref && (!data || !isEqual(data.questionnaireData, newData));
×
245
  };
×
246

247
  const isAllSectionsComplete = (): boolean => {
29✔
248
    if (status === FormStatus.LOADING) {
11!
249
      return false;
×
250
    }
×
251

252
    // form has not been created
253
    if (
11✔
254
      !data?.questionnaireData ||
11✔
255
      data?.questionnaireData?.sections?.length !== Object.keys(map).length - 1
11✔
256
    ) {
11!
257
      return false;
×
258
    }
×
259

260
    return data?.questionnaireData?.sections?.every((section) => section.status === "Completed");
11✔
261
  };
11✔
262

263
  /**
264
   * submit the form data to the database.
265
   *
266
   * @returns {Promise<boolean>} true if the submit was successful, false otherwise
267
   */
268
  const submitForm = async (): Promise<string | boolean> => {
29✔
269
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
270

271
    if (!ref?.current || !newData) {
×
272
      return false;
×
273
    }
×
274

275
    try {
×
276
      const r = await submitData();
×
277
      setOpenSubmitDialog(false);
×
278
      navigate("/submission-requests");
×
279

280
      return r;
×
281
    } catch (err) {
×
282
      setOpenSubmitDialog(false);
×
283
      enqueueSnackbar("An error occurred while submitting the form. Please try again.", {
×
284
        variant: "error",
×
285
      });
×
286
      return false;
×
287
    }
×
288
  };
×
289

290
  /**
291
   * submit the approval comment from the form submission to the database.
292
   *
293
   * @returns {Promise<boolean>} true if the approval submission was successful, false otherwise
294
   */
295
  const submitApproveForm = async (reviewComment: string): Promise<string | boolean> => {
29✔
296
    if (formMode !== "Review") {
1!
297
      return false;
×
298
    }
×
299
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
1!
300

301
    if (!ref?.current || !newData) {
1!
302
      return false;
×
303
    }
×
304

305
    const res = await approveForm(
1✔
306
      { reviewComment, pendingModelChange, pendingImageDeIdentification },
1✔
307
      true
1✔
308
    );
1✔
309
    setOpenApproveDialog(false);
1✔
310
    setPendingModelChange(false);
1✔
311
    setPendingImageDeIdentification(false);
1✔
312
    if (res?.status === "success") {
1!
313
      navigate("/submission-requests");
×
314
    } else {
1✔
315
      enqueueSnackbar(
1✔
316
        res?.errorMessage || "An error occurred while approving the form. Please try again.",
1!
317
        {
1✔
318
          variant: "error",
1✔
319
        }
1✔
320
      );
1✔
321
    }
1✔
322
    return res?.status === "success";
1!
323
  };
1✔
324

325
  /**
326
   * submit the inquire comment from the form submission to the database.
327
   *
328
   *
329
   * @returns {Promise<boolean>} true if the inquire submission was successful, false otherwise
330
   */
331
  const submitInquireForm = async (reviewComment: string): Promise<string | boolean> => {
29✔
332
    if (formMode !== "Review") {
×
333
      return false;
×
334
    }
×
335
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
336

337
    if (!ref?.current || !newData) {
×
338
      return false;
×
339
    }
×
340

341
    const res = await inquireForm(reviewComment);
×
342
    if (!res) {
×
343
      enqueueSnackbar("An error occurred while inquiring the form. Please try again.", {
×
344
        variant: "error",
×
345
      });
×
346
    } else {
×
347
      navigate("/submission-requests");
×
348
    }
×
349
    setOpenInquireDialog(false);
×
350
    return res;
×
351
  };
×
352

353
  /**
354
   * submit the reject comment from the form submission to the database.
355
   *
356
   *
357
   * @returns {Promise<boolean>} true if the reject submission was successful, false otherwise
358
   */
359
  const submitRejectForm = async (reviewComment: string): Promise<string | boolean> => {
29✔
360
    if (formMode !== "Review") {
×
361
      return false;
×
362
    }
×
363
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
364

365
    if (!ref?.current || !newData) {
×
366
      return false;
×
367
    }
×
368

369
    const res = await rejectForm(reviewComment);
×
370
    if (!res) {
×
371
      enqueueSnackbar("An error occurred while rejecting the form. Please try again.", {
×
372
        variant: "error",
×
373
      });
×
374
    } else {
×
375
      navigate("/submission-requests");
×
376
    }
×
377
    setOpenRejectDialog(false);
×
378
    return res;
×
379
  };
×
380

381
  /**
382
   * Reopen the form when it has already been inquired
383
   * and the user wants to retry submission
384
   *
385
   *
386
   * @returns {Promise<boolean>} true if the review submit was successful, false otherwise
387
   */
388
  const handleReopenForm = async (): Promise<string | boolean> => {
29✔
389
    if (formMode !== "Edit") {
×
390
      return false;
×
391
    }
×
392
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
393

394
    if (!ref?.current || !newData) {
×
395
      return false;
×
396
    }
×
397

398
    const res = await reopenForm();
×
399
    if (!res) {
×
400
      navigate("/submission-requests", {
×
401
        state: {
×
402
          error: "An error occurred while marking the form as In Progress. Please try again.",
×
403
        },
×
404
      });
×
405
    }
×
406
    return res;
×
407
  };
×
408

409
  /**
410
   * Saves the form data to the database.
411
   *
412
   * NOTE:
413
   * - This function relies on HTML5 reportValidity() to
414
   *   validate the form section status.
415
   *
416
   * @returns {Promise<boolean>} true if the save was successful, false otherwise
417
   */
418
  const saveForm = async (): Promise<SaveForm> => {
29✔
419
    if (readOnlyInputs || formMode !== "Edit") {
×
420
      return {
×
421
        status: "failed",
×
422
        errorMessage: null,
×
423
      };
×
424
    }
×
425

426
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
427
    if (!ref?.current || !newData) {
×
428
      return {
×
429
        status: "failed",
×
430
        errorMessage: null,
×
431
      };
×
432
    }
×
433

434
    // Update section status
435
    if (newData?.sections?.length !== Object.keys(map).length - 1) {
×
436
      // Not including review section
437
      newData.sections = cloneDeep(InitialSections);
×
438
    }
×
439

440
    const newStatus: SectionStatus = determineSectionStatus(
×
441
      ref.current.checkValidity(),
×
442
      sectionHasData(activeSection, newData)
×
443
    );
×
444
    const currentSection: Section = newData.sections.find((s) => s.name === activeSection);
×
445
    if (currentSection) {
×
446
      currentSection.status = newStatus;
×
447
    } else {
×
448
      newData.sections.push({ name: activeSection, status: newStatus });
×
449
    }
×
450

451
    const saveResult = await setData(newData);
×
452
    if (saveResult?.status === "failed" && !!saveResult?.errorMessage) {
×
453
      enqueueSnackbar(`An error occurred while saving the ${map[activeSection].title} section.`, {
×
454
        variant: "error",
×
455
      });
×
456
    } else {
×
NEW
457
      enqueueSnackbar(
×
NEW
458
        `Your changes for the ${map[activeSection].title} section have been successfully saved.`,
×
NEW
459
        {
×
NEW
460
          variant: "success",
×
UNCOV
461
        }
×
UNCOV
462
      );
×
463
    }
×
464

465
    if (saveResult?.status === "success") {
×
466
      return {
×
467
        status: "success",
×
468
        id: saveResult.id,
×
UNCOV
469
      };
×
470
    }
×
471

472
    return {
×
473
      status: "failed",
×
474
      errorMessage: saveResult?.errorMessage,
×
UNCOV
475
    };
×
UNCOV
476
  };
×
477

478
  // Intercept React Router navigation actions with unsaved changes
479
  const blocker: Blocker = useBlocker(() => {
29✔
480
    // if unauthorized, skip blocker and redirect away
481
    if (
×
482
      formMode === "Unauthorized" &&
×
483
      status === FormStatus.LOADED &&
×
484
      authStatus === AuthStatus.LOADED
×
485
    ) {
×
486
      return false;
×
487
    }
×
488
    if (!isDirty() || readOnlyInputs || bypassBlockerRef.current) {
×
UNCOV
489
      return false;
×
UNCOV
490
    }
×
491

492
    // If there are no validation errors, save form data without a prompt
493
    const { ref } = refs.getFormObjectRef.current?.() || {};
×
494
    if (ref?.current?.checkValidity() === true) {
×
495
      saveForm();
×
UNCOV
496
      return false;
×
497
    }
×
498

UNCOV
499
    setBlockedNavigate(true);
×
UNCOV
500
    return true;
×
501
  });
29✔
502

503
  const isNavigationBlocked = useMemo<boolean>(() => blocker?.state === "blocked", [blocker]);
29✔
504

505
  const areSectionsValid = (): boolean => {
29✔
506
    if (status === FormStatus.LOADING) {
×
UNCOV
507
      return false;
×
508
    }
×
509

510
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
511

512
    if (!ref?.current || !newData) {
×
UNCOV
513
      return false;
×
514
    }
×
515

UNCOV
516
    const sectionsClone = cloneDeep(data?.questionnaireData?.sections);
×
517
    if (sectionsClone?.length !== Object.keys(map).length - 1) {
×
518
      // Not including review section
UNCOV
519
      return false;
×
520
    }
×
521

522
    const newStatus = ref.current.checkValidity() ? "Completed" : "In Progress";
×
523
    const currentSection: Section = sectionsClone.find((s) => s.name === activeSection);
×
524
    if (currentSection) {
×
525
      currentSection.status = newStatus;
×
526
    } else {
×
UNCOV
527
      sectionsClone.push({ name: activeSection, status: newStatus });
×
528
    }
×
529

UNCOV
530
    return sectionsClone?.every((section) => section.status === "Completed");
×
UNCOV
531
  };
×
532

533
  const saveAndNavigate = async (): Promise<void> => {
29✔
534
    // Wait for the save handler to complete
535
    const res = await saveForm();
×
UNCOV
536
    const reviewSectionUrl = `/submission-request/${data._id}/REVIEW`; // TODO: Update to dynamic url instead
×
537
    const isNavigatingToReviewSection = blocker?.location?.pathname === reviewSectionUrl;
×
538

UNCOV
539
    setBlockedNavigate(false);
×
540

541
    // if invalid data, then block navigation
542
    if (
×
543
      isNavigatingToReviewSection &&
×
544
      ((res?.status === "success" && !res?.id) || !areSectionsValid())
×
545
    ) {
×
UNCOV
546
      return;
×
547
    }
×
548

UNCOV
549
    blocker.proceed?.();
×
UNCOV
550
  };
×
551

552
  /**
553
   * Provides a discard handler for the Unsaved Changes dialog.
554
   * Will discard the form changes and then navigate to the blocked section.
555
   */
556
  const discardAndNavigate = (): void => {
29✔
557
    setBlockedNavigate(false);
×
UNCOV
558
    blocker.proceed?.();
×
UNCOV
559
  };
×
560

561
  const handleSubmitForm = () => {
29✔
562
    if (!hasPermission(user, "submission_request", "submit", data)) {
×
563
      Logger.error("Invalid request to submit Submission Request form.");
×
564
      return;
×
565
    }
×
UNCOV
566
    setOpenSubmitDialog(true);
×
UNCOV
567
  };
×
568

569
  const handleApproveForm = () => {
29✔
570
    if (formMode !== "Review") {
2!
UNCOV
571
      return;
×
UNCOV
572
    }
×
573
    setOpenApproveDialog(true);
2✔
574
  };
2✔
575

576
  const handleInquireForm = () => {
29✔
577
    if (formMode !== "Review") {
1!
UNCOV
578
      return;
×
UNCOV
579
    }
×
580
    setOpenInquireDialog(true);
1✔
581
  };
1✔
582

583
  const handleRejectForm = () => {
29✔
584
    if (formMode !== "Review") {
1!
UNCOV
585
      return;
×
UNCOV
586
    }
×
587
    setOpenRejectDialog(true);
1✔
588
  };
1✔
589

590
  const handleCloseApproveFormDialog = () => {
29✔
591
    setOpenApproveDialog(false);
×
592
    setPendingModelChange(false);
×
UNCOV
593
    setPendingImageDeIdentification(false);
×
UNCOV
594
  };
×
595

596
  const handleCloseInquireFormDialog = () => {
29✔
UNCOV
597
    setOpenInquireDialog(false);
×
UNCOV
598
  };
×
599

600
  const handleCloseRejectFormDialog = () => {
29✔
UNCOV
601
    setOpenRejectDialog(false);
×
UNCOV
602
  };
×
603

604
  const handleBackClick = () => {
29✔
605
    if (status === FormStatus.SAVING || !prevSection) {
×
606
      return;
×
607
    }
×
UNCOV
608
    navigate(prevSection, { preventScrollReset: true });
×
UNCOV
609
  };
×
610

611
  const handleNextClick = () => {
29✔
612
    if (status === FormStatus.SAVING || !nextSection) {
×
613
      return;
×
614
    }
×
615
    if (isSectionD && !allSectionsComplete) {
×
616
      return;
×
617
    }
×
UNCOV
618
    navigate(nextSection, { preventScrollReset: true });
×
UNCOV
619
  };
×
620

621
  const handleOnCancel = useCallback(() => {
29✔
622
    bypassBlockerRef.current = true;
×
623
    enqueueSnackbar("Successfully canceled that Submission Request.", {
×
624
      variant: "success",
×
UNCOV
625
    });
×
UNCOV
626
    navigate("/submission-requests");
×
627
  }, [enqueueSnackbar, navigate]);
29✔
628

629
  useEffect(() => {
29✔
630
    const newSection = validateSection(section) ? section : "A";
10!
631
    setActiveSection(newSection);
10✔
632
    lastSectionRef.current = newSection;
10✔
633
  }, [section]);
29✔
634

635
  // Intercept browser navigation actions (e.g. closing the tab) with unsaved changes
636
  useEffect(() => {
29✔
637
    const unloadHandler = (event: BeforeUnloadEvent) => {
29✔
638
      if (!isDirty() || bypassBlockerRef.current) {
×
UNCOV
639
        return;
×
UNCOV
640
      }
×
641

642
      // If there are no validation errors, save form data without a prompt
643
      const { ref } = refs.getFormObjectRef.current?.() || {};
×
644
      if (ref?.current?.checkValidity() === true) {
×
645
        saveForm();
×
UNCOV
646
        return;
×
647
      }
×
648

649
      event.preventDefault();
×
UNCOV
650
      event.returnValue = "You have unsaved form changes. Are you sure you want to leave?";
×
UNCOV
651
    };
×
652

653
    window.addEventListener("beforeunload", unloadHandler);
29✔
654

655
    return () => {
29✔
656
      window.removeEventListener("beforeunload", unloadHandler);
29✔
657
    };
29✔
658
  });
29✔
659

660
  useEffect(() => {
29✔
661
    const isComplete = isAllSectionsComplete();
11✔
662
    setAllSectionsComplete(isComplete);
11✔
663
  }, [status, data]);
29✔
664

665
  useEffect(() => {
29✔
666
    if (status !== FormStatus.LOADED && authStatus !== AuthStatus.LOADED) {
10!
UNCOV
667
      return;
×
668
    }
×
669
    if (!hasReopenedFormRef.current && data?.status === "Inquired" && formMode === "Edit") {
10!
670
      handleReopenForm();
×
UNCOV
671
      hasReopenedFormRef.current = true;
×
UNCOV
672
    }
×
673
  }, [status, authStatus, formMode, data?.status]);
29✔
674

675
  useEffect(() => {
29✔
676
    // Skip URL overwrite if the form isn't loaded, or the blocker is active
677
    if (!data || isNavigationBlocked) {
11!
UNCOV
678
      return;
×
UNCOV
679
    }
×
680

681
    if (previousIDRef.current === "new" && data?._id !== "new") {
11✔
682
      Logger.info("Form created with new ID. Redirecting to new form URL.", { uuid: data._id });
1✔
683
      bypassBlockerRef.current = true;
1✔
684
      navigate(
1✔
685
        location.pathname.replace("/submission-request/new", `/submission-request/${data._id}`),
1✔
686
        { replace: true, preventScrollReset: true }
1✔
687
      );
1✔
688
      bypassBlockerRef.current = false;
1✔
689
    }
1✔
690

691
    previousIDRef.current = data._id;
11✔
692
  }, [data?._id, isNavigationBlocked]);
29✔
693

694
  // Show loading spinner if the form is still loading
695
  if (status === FormStatus.LOADING || authStatus === AuthStatus.LOADING) {
29!
UNCOV
696
    return <SuspenseLoader />;
×
UNCOV
697
  }
×
698

699
  // Hide form content if the user is unauthorized
700
  if (authStatus === AuthStatus.ERROR) {
29!
UNCOV
701
    return null;
×
UNCOV
702
  }
×
703

704
  // Redirect to ListView if no data is found and the form is in the error state
705
  if (status === FormStatus.ERROR && !data?._id) {
29!
UNCOV
706
    return <Navigate to="/submission-requests" state={{ error: error || "Unknown error" }} />;
×
UNCOV
707
  }
×
708

709
  return (
29✔
710
    <>
29✔
711
      <PageBanner
29✔
712
        title="Submission Request Form"
29✔
713
        subTitle="The following set of high-level questions are intended to provide insight to the CRDC, related to data storage, access, secondary sharing needs and other requirements of data submitters."
29✔
714
        bannerSrc={bannerPng}
29✔
715
      />
716

717
      <StyledContainer ref={formContentRef} maxWidth="xl">
29✔
718
        <StyledContentWrapper direction="row" justifyContent="center">
29✔
719
          <StyledSidebar direction="row" justifyContent="center" alignSelf="flex-start">
29✔
720
            <ProgressBar section={activeSection} />
29✔
721
            <StyledDivider orientation="vertical" />
29✔
722
          </StyledSidebar>
29✔
723

724
          <StyledContent direction="column" spacing={5}>
29✔
725
            <StatusBar />
29✔
726

727
            <Section section={activeSection} refs={refs} />
29✔
728

729
            <StyledControls direction="row" justifyContent="center" alignItems="center" spacing={2}>
29✔
730
              <StyledLoadingButton
29✔
731
                id="submission-form-back-button"
29✔
732
                variant="contained"
29✔
733
                color="info"
29✔
734
                type="button"
29✔
735
                disabled={status === FormStatus.SAVING || !prevSection}
29✔
736
                onClick={handleBackClick}
29✔
737
                size="large"
29✔
738
                startIcon={<ChevronLeft />}
29✔
739
              >
740
                Back
741
              </StyledLoadingButton>
29✔
742

743
              {activeSection === "REVIEW" &&
29✔
744
                hasPermission(user, "submission_request", "submit", data) && (
21!
745
                  <StyledExtendedLoadingButton
×
746
                    id="submission-form-submit-button"
×
747
                    variant="contained"
×
748
                    color="primary"
×
749
                    type="submit"
×
UNCOV
750
                    size="large"
×
UNCOV
751
                    onClick={handleSubmitForm}
×
752
                  >
753
                    Submit
UNCOV
754
                  </StyledExtendedLoadingButton>
×
755
                )}
756
              {activeSection !== "REVIEW" && formMode === "Edit" && (
29✔
757
                <StyledLoadingButton
8✔
758
                  id="submission-form-save-button"
8✔
759
                  variant="contained"
8✔
760
                  color="success"
8✔
761
                  loading={status === FormStatus.SAVING}
8✔
762
                  onClick={saveForm}
8✔
763
                >
764
                  Save
765
                </StyledLoadingButton>
8✔
766
              )}
767

768
              <CancelApplicationButton onCancel={handleOnCancel} />
29✔
769

770
              {activeSection !== "REVIEW" && (
29✔
771
                <StyledTooltip
8✔
772
                  title={
8✔
773
                    shouldShowToolTip
8!
UNCOV
774
                      ? "Click ‘Save’ to validate your answers. Once all pages are complete, you can proceed to the Review page to submit."
×
775
                      : ""
8✔
776
                  }
777
                  placement="top"
8✔
778
                  arrow
8✔
779
                  disableHoverListener={!shouldShowToolTip}
8✔
780
                >
781
                  <span>
8✔
782
                    <StyledLoadingButton
8✔
783
                      id="submission-form-next-button"
8✔
784
                      variant="contained"
8✔
785
                      color="info"
8✔
786
                      type="button"
8✔
787
                      onClick={handleNextClick}
8✔
788
                      disabled={status === FormStatus.SAVING || !nextSection || shouldShowToolTip}
8✔
789
                      size="large"
8✔
790
                      endIcon={<ChevronRight />}
8✔
791
                    >
792
                      Next
793
                    </StyledLoadingButton>
8✔
794
                  </span>
8✔
795
                </StyledTooltip>
8✔
796
              )}
797

798
              {activeSection === "REVIEW" && formMode === "Review" && (
29✔
799
                <>
21✔
800
                  <StyledExtendedLoadingButton
21✔
801
                    id="submission-form-approve-button"
21✔
802
                    variant="contained"
21✔
803
                    color="primary"
21✔
804
                    size="large"
21✔
805
                    onClick={handleApproveForm}
21✔
806
                  >
807
                    Approve
808
                  </StyledExtendedLoadingButton>
21✔
809
                  <StyledExtendedLoadingButton
21✔
810
                    id="submission-form-reject-button"
21✔
811
                    variant="contained"
21✔
812
                    color="error"
21✔
813
                    size="large"
21✔
814
                    onClick={handleRejectForm}
21✔
815
                  >
816
                    Reject
817
                  </StyledExtendedLoadingButton>
21✔
818
                  <StyledLoadingButton
21✔
819
                    id="submission-form-inquire-button"
21✔
820
                    variant="contained"
21✔
821
                    color="info"
21✔
822
                    size="large"
21✔
823
                    onClick={handleInquireForm}
21✔
824
                  >
825
                    Request Additional Information
826
                  </StyledLoadingButton>
21✔
827
                </>
21✔
828
              )}
829
            </StyledControls>
29✔
830
          </StyledContent>
29✔
831
        </StyledContentWrapper>
29✔
832
      </StyledContainer>
29✔
833

834
      <UnsavedChangesDialog
29✔
835
        open={blockedNavigate}
29✔
836
        onCancel={() => setBlockedNavigate(false)}
29✔
837
        onSave={saveAndNavigate}
29✔
838
        onDiscard={discardAndNavigate}
29✔
839
        disableActions={status === FormStatus.SAVING}
29✔
840
      />
841
      <SubmitFormDialog
29✔
842
        open={openSubmitDialog}
29✔
843
        onCancel={() => setOpenSubmitDialog(false)}
29✔
844
        onSubmit={submitForm}
29✔
845
        disableActions={status === FormStatus.SUBMITTING}
29✔
846
      />
847
      <ReviewFormDialog
29✔
848
        open={openApproveDialog}
29✔
849
        header="Approve Submission Request"
29✔
850
        confirmText="Confirm to Approve"
29✔
851
        confirmButtonProps={{ color: "success" }}
29✔
852
        onCancel={handleCloseApproveFormDialog}
29✔
853
        onSubmit={(reviewComment) => submitApproveForm(reviewComment)}
29✔
854
        loading={status === FormStatus.SUBMITTING}
29✔
855
      >
856
        <Stack direction="column">
29✔
857
          <FormControlLabel
29✔
858
            control={
29✔
859
              <StyledCheckbox
29✔
860
                checked={pendingModelChange}
29✔
861
                onChange={(e) => setPendingModelChange(e.target.checked)}
29✔
862
                checkedIcon={<CheckedIcon readOnly={status === FormStatus.SUBMITTING} />}
29✔
863
                icon={<UncheckedIcon readOnly={status === FormStatus.SUBMITTING} />}
29✔
864
                disabled={status === FormStatus.SUBMITTING}
29✔
865
                inputProps={
29✔
866
                  { "data-testid": "pendingModelChange-checkbox" } as CheckboxProps["inputProps"]
29✔
867
                }
29✔
868
              />
869
            }
870
            label="Require Data Model changes"
29✔
871
          />
872
          <FormControlLabel
29✔
873
            control={
29✔
874
              <StyledCheckbox
29✔
875
                checked={pendingImageDeIdentification}
29✔
876
                onChange={(e) => setPendingImageDeIdentification(e.target.checked)}
29✔
877
                checkedIcon={<CheckedIcon readOnly={status === FormStatus.SUBMITTING} />}
29✔
878
                icon={<UncheckedIcon readOnly={status === FormStatus.SUBMITTING} />}
29✔
879
                disabled={status === FormStatus.SUBMITTING}
29✔
880
                inputProps={
29✔
881
                  {
29✔
882
                    "data-testid": "pendingImageDeIdentification-checkbox",
29✔
883
                  } as CheckboxProps["inputProps"]
29✔
884
                }
29✔
885
              />
886
            }
887
            label="Require Risk Mitigation document & De-identification protocol"
29✔
888
          />
889
        </Stack>
29✔
890
      </ReviewFormDialog>
29✔
891
      <ReviewFormDialog
29✔
892
        open={openInquireDialog}
29✔
893
        header="Request Additional Changes"
29✔
894
        confirmText="Confirm to move to Inquired"
29✔
895
        onCancel={handleCloseInquireFormDialog}
29✔
896
        onSubmit={(reviewComment) => submitInquireForm(reviewComment)}
29✔
897
      />
898
      <ReviewFormDialog
29✔
899
        open={openRejectDialog}
29✔
900
        header="Reject Submission Request"
29✔
901
        confirmText="Confirm to Reject"
29✔
902
        onCancel={handleCloseRejectFormDialog}
29✔
903
        onSubmit={(reviewComment) => submitRejectForm(reviewComment)}
29✔
904
      />
905
    </>
29✔
906
  );
907
};
29✔
908

909
export default FormView;
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