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

CBIIT / crdc-datahub-ui / 14666777147

25 Apr 2025 02:19PM UTC coverage: 62.713%. First build
14666777147

Pull #691

github

web-flow
Merge 8956c2f3e into cbeb99a86
Pull Request #691: CRDCDH-2594 Add support for auth permission scoping

3452 of 5899 branches covered (58.52%)

Branch coverage included in aggregate %.

68 of 84 new or added lines in 17 files covered. (80.95%)

4776 of 7221 relevant lines covered (66.14%)

196.49 hits per line

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

0.0
/src/content/questionnaire/FormView.tsx
1
import React, { FC, useEffect, useRef, useState } from "react";
2
import {
3
  useNavigate,
4
  unstable_useBlocker as useBlocker,
5
  unstable_Blocker as Blocker,
6
  Navigate,
7
} from "react-router-dom";
8
import { isEqual, cloneDeep } from "lodash";
9
import { Container, Divider, Stack, styled } from "@mui/material";
10
import { LoadingButton } from "@mui/lab";
11
import { useSnackbar } from "notistack";
12
import { ReactComponent as ChevronLeft } from "../../assets/icons/chevron_left.svg";
13
import { ReactComponent as ChevronRight } from "../../assets/icons/chevron_right.svg";
14
import { Status as FormStatus, useFormContext } from "../../components/Contexts/FormContext";
15
import SuspenseLoader from "../../components/SuspenseLoader";
16
import StatusBar from "../../components/StatusBar/StatusBar";
17
import ProgressBar from "../../components/ProgressBar/ProgressBar";
18
import Section from "./sections";
19
import map, { InitialSections } from "../../config/SectionConfig";
20
import UnsavedChangesDialog from "../../components/Questionnaire/UnsavedChangesDialog";
21
import SubmitFormDialog from "../../components/Questionnaire/SubmitFormDialog";
22
import useFormMode from "../../hooks/useFormMode";
23
import InquireFormDialog from "../../components/Questionnaire/InquireFormDialog";
24
import RejectFormDialog from "../../components/Questionnaire/RejectFormDialog";
25
import ApproveFormDialog from "../../components/Questionnaire/ApproveFormDialog";
26
import PageBanner from "../../components/PageBanner";
27
import bannerPng from "../../assets/banner/submission_banner.png";
28
import { Status as AuthStatus, useAuthContext } from "../../components/Contexts/AuthContext";
29
import usePageTitle from "../../hooks/usePageTitle";
30
import ExportRequestButton from "../../components/ExportRequestButton";
31
import { Logger } from "../../utils";
32
import { hasPermission } from "../../config/AuthPermissions";
33

34
const StyledContainer = styled(Container)(() => ({
×
35
  "&.MuiContainer-root": {
36
    padding: 0,
37
    minHeight: "300px",
38
    scrollMarginTop: "-60px",
39
  },
40
}));
41

42
const StyledSidebar = styled(Stack)({
×
43
  position: "sticky",
44
  top: 0,
45
  marginTop: "90px",
46
});
47

48
const StyledDivider = styled(Divider)({
×
49
  height: "520px",
50
  width: "1px",
51
  borderRightWidth: "2px",
52
  borderRightColor: "#E8EAEE9",
53
  margin: "0 23px",
54
});
55

56
const StyledContentWrapper = styled(Stack)({
×
57
  paddingBottom: "75px",
58
});
59

60
const StyledContent = styled(Stack)({
×
61
  width: "100%",
62
  maxWidth: "980px",
63
  marginLeft: "41px",
64
});
65

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

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

115
const StyledExtendedLoadingButton = styled(StyledLoadingButton)({
×
116
  "&.MuiButton-root": {
117
    minWidth: "137px",
118
  },
119
});
120

121
const validateSection = (section: string) => typeof map[section] !== "undefined";
×
122

123
export type SaveForm =
124
  | { status: "success"; id: string }
125
  | { status: "failed"; errorMessage: string };
126

127
type Props = {
128
  section?: string;
129
};
130

131
/**
132
 * Intake Form View Component
133
 *
134
 * @param {Props} props
135
 * @returns {JSX.Element}
136
 */
137
const FormView: FC<Props> = ({ section }: Props) => {
×
138
  const navigate = useNavigate();
×
139
  const { enqueueSnackbar } = useSnackbar();
×
140
  const {
141
    status,
142
    data,
143
    setData,
144
    submitData,
145
    approveForm,
146
    inquireForm,
147
    rejectForm,
148
    reopenForm,
149
    error,
150
  } = useFormContext();
×
151
  const { user, status: authStatus } = useAuthContext();
×
152
  const { formMode, readOnlyInputs } = useFormMode();
×
153

154
  const [activeSection, setActiveSection] = useState<string>(
×
155
    validateSection(section) ? section : "A"
×
156
  );
157
  const [blockedNavigate, setBlockedNavigate] = useState<boolean>(false);
×
158
  const [openSubmitDialog, setOpenSubmitDialog] = useState<boolean>(false);
×
159
  const [openApproveDialog, setOpenApproveDialog] = useState<boolean>(false);
×
160
  const [openInquireDialog, setOpenInquireDialog] = useState<boolean>(false);
×
161
  const [openRejectDialog, setOpenRejectDialog] = useState<boolean>(false);
×
162
  const [allSectionsComplete, setAllSectionsComplete] = useState<boolean>(false);
×
163

164
  const sectionKeys = Object.keys(map);
×
165
  const sectionIndex = sectionKeys.indexOf(activeSection);
×
166
  const prevSection = sectionKeys[sectionIndex - 1]
×
167
    ? `/submission-request/${data?.["_id"]}/${sectionKeys[sectionIndex - 1]}`
168
    : null;
169
  const nextSection = sectionKeys[sectionIndex + 1]
×
170
    ? `/submission-request/${data?.["_id"]}/${sectionKeys[sectionIndex + 1]}`
171
    : null;
172
  const isSectionD = activeSection === "D";
×
173
  const formContentRef = useRef(null);
×
174
  const lastSectionRef = useRef(null);
×
175
  const hasReopenedFormRef = useRef(false);
×
176

177
  const refs: FormSectionProps["refs"] = {
×
178
    getFormObjectRef: useRef<(() => FormObject) | null>(null),
179
  };
180

181
  usePageTitle(`Submission Request ${data?._id || ""}`);
×
182

183
  /**
184
   * Determines if the form has unsaved changes.
185
   *
186
   * @returns {boolean} true if the form has unsaved changes, false otherwise
187
   */
188
  const isDirty = (): boolean => {
×
189
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
190

191
    return ref && (!data || !isEqual(data.questionnaireData, newData));
×
192
  };
193

194
  useEffect(() => {
×
195
    const newSection = validateSection(section) ? section : "A";
×
196
    setActiveSection(newSection);
×
197
    lastSectionRef.current = newSection;
×
198
  }, [section]);
199

200
  const isAllSectionsComplete = (): boolean => {
×
201
    if (status === FormStatus.LOADING) {
×
202
      return false;
×
203
    }
204

205
    // form has not been created
206
    if (
×
207
      !data?.questionnaireData ||
×
208
      data?.questionnaireData?.sections?.length !== Object.keys(map).length - 1
209
    ) {
210
      return false;
×
211
    }
212

213
    return data?.questionnaireData?.sections?.every((section) => section.status === "Completed");
×
214
  };
215

216
  /**
217
   * submit the form data to the database.
218
   *
219
   * @returns {Promise<boolean>} true if the submit was successful, false otherwise
220
   */
221
  const submitForm = async (): Promise<string | boolean> => {
×
222
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
223

224
    if (!ref?.current || !newData) {
×
225
      return false;
×
226
    }
227

228
    try {
×
229
      const r = await submitData();
×
230
      setOpenSubmitDialog(false);
×
231
      navigate("/submission-requests");
×
232

233
      return r;
×
234
    } catch (err) {
235
      setOpenSubmitDialog(false);
×
236
      enqueueSnackbar("An error occurred while submitting the form. Please try again.", {
×
237
        variant: "error",
238
      });
239
      return false;
×
240
    }
241
  };
242

243
  /**
244
   * submit the approval comment from the form submission to the database.
245
   *
246
   * @returns {Promise<boolean>} true if the approval submission was successful, false otherwise
247
   */
248
  const submitApproveForm = async (reviewComment): Promise<string | boolean> => {
×
249
    if (formMode !== "Review") {
×
250
      return false;
×
251
    }
252
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
253

254
    if (!ref?.current || !newData) {
×
255
      return false;
×
256
    }
257

258
    const res = await approveForm(reviewComment, true);
×
259
    setOpenApproveDialog(false);
×
260
    if (res?.status === "success") {
×
261
      navigate("/submission-requests");
×
262
    } else {
263
      enqueueSnackbar(
×
264
        res.errorMessage || "An error occurred while approving the form. Please try again.",
×
265
        {
266
          variant: "error",
267
        }
268
      );
269
    }
270
    return res.status === "success";
×
271
  };
272

273
  /**
274
   * submit the inquire comment from the form submission to the database.
275
   *
276
   *
277
   * @returns {Promise<boolean>} true if the inquire submission was successful, false otherwise
278
   */
279
  const submitInquireForm = async (reviewComment: string): Promise<string | boolean> => {
×
280
    if (formMode !== "Review") {
×
281
      return false;
×
282
    }
283
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
284

285
    if (!ref?.current || !newData) {
×
286
      return false;
×
287
    }
288

289
    const res = await inquireForm(reviewComment);
×
290
    if (!res) {
×
291
      enqueueSnackbar("An error occurred while inquiring the form. Please try again.", {
×
292
        variant: "error",
293
      });
294
    } else {
295
      navigate("/submission-requests");
×
296
    }
297
    setOpenInquireDialog(false);
×
298
    return res;
×
299
  };
300

301
  /**
302
   * submit the reject comment from the form submission to the database.
303
   *
304
   *
305
   * @returns {Promise<boolean>} true if the reject submission was successful, false otherwise
306
   */
307
  const submitRejectForm = async (reviewComment: string): Promise<string | boolean> => {
×
308
    if (formMode !== "Review") {
×
309
      return false;
×
310
    }
311
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
312

313
    if (!ref?.current || !newData) {
×
314
      return false;
×
315
    }
316

317
    const res = await rejectForm(reviewComment);
×
318
    if (!res) {
×
319
      enqueueSnackbar("An error occurred while rejecting the form. Please try again.", {
×
320
        variant: "error",
321
      });
322
    } else {
323
      navigate("/submission-requests");
×
324
    }
325
    setOpenRejectDialog(false);
×
326
    return res;
×
327
  };
328

329
  /**
330
   * Reopen the form when it has already been inquired
331
   * and the user wants to retry submission
332
   *
333
   *
334
   * @returns {Promise<boolean>} true if the review submit was successful, false otherwise
335
   */
336
  const handleReopenForm = async (): Promise<string | boolean> => {
×
337
    if (formMode !== "Edit") {
×
338
      return false;
×
339
    }
340
    const { ref, data: newData } = refs.getFormObjectRef.current?.() || {};
×
341

342
    if (!ref?.current || !newData) {
×
343
      return false;
×
344
    }
345

346
    const res = await reopenForm();
×
347
    if (!res) {
×
348
      navigate("/submission-requests", {
×
349
        state: {
350
          error: "An error occurred while marking the form as In Progress. Please try again.",
351
        },
352
      });
353
    }
354
    return res;
×
355
  };
356

357
  /**
358
   * Saves the form data to the database.
359
   *
360
   * NOTE:
361
   * - This function relies on HTML5 reportValidity() to
362
   *   validate the form section status.
363
   *
364
   * @returns {Promise<boolean>} true if the save was successful, false otherwise
365
   */
366
  const saveForm = async (): Promise<SaveForm> => {
×
367
    if (readOnlyInputs || formMode !== "Edit") {
×
368
      return {
×
369
        status: "failed",
370
        errorMessage: null,
371
      };
372
    }
373

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

376
    if (!ref?.current || !newData) {
×
377
      return {
×
378
        status: "failed",
379
        errorMessage: null,
380
      };
381
    }
382

383
    // Update section status
384
    if (newData?.sections?.length !== Object.keys(map).length - 1) {
×
385
      // Not including review section
386
      newData.sections = cloneDeep(InitialSections);
×
387
    }
388
    const newStatus = ref.current.checkValidity() ? "Completed" : "In Progress";
×
389
    const currentSection: Section = newData.sections.find((s) => s.name === activeSection);
×
390
    if (currentSection) {
×
391
      currentSection.status = newStatus;
×
392
    } else {
393
      newData.sections.push({ name: activeSection, status: newStatus });
×
394
    }
395

396
    const saveResult = await setData(newData);
×
397
    if (saveResult?.status === "failed" && !!saveResult?.errorMessage) {
×
398
      enqueueSnackbar(`An error occurred while saving the ${map[activeSection].title} section.`, {
×
399
        variant: "error",
400
      });
401
    } else {
402
      enqueueSnackbar(
×
403
        `Your changes for the ${map[activeSection].title} section have been successfully saved.`,
404
        {
405
          variant: "success",
406
        }
407
      );
408
    }
409

410
    if (
×
411
      !blockedNavigate &&
×
412
      saveResult?.status === "success" &&
413
      data["_id"] === "new" &&
414
      saveResult.id !== data?.["_id"]
415
    ) {
416
      // NOTE: This currently triggers a form data refetch, which is not ideal
417
      navigate(`/submission-request/${saveResult.id}/${activeSection}`, {
×
418
        replace: true,
419
        preventScrollReset: true,
420
      });
421
    }
422

423
    if (saveResult?.status === "success") {
×
424
      return {
×
425
        status: "success",
426
        id: saveResult.id,
427
      };
428
    }
429

430
    return {
×
431
      status: "failed",
432
      errorMessage: saveResult?.errorMessage,
433
    };
434
  };
435

436
  // Intercept React Router navigation actions with unsaved changes
437
  const blocker: Blocker = useBlocker(() => {
×
438
    // if unauthorized, skip blocker and redirect away
439
    if (
×
440
      formMode === "Unauthorized" &&
×
441
      status === FormStatus.LOADED &&
442
      authStatus === AuthStatus.LOADED
443
    ) {
444
      return false;
×
445
    }
446
    if (!isDirty() || readOnlyInputs) {
×
447
      return false;
×
448
    }
449

450
    // If there are no validation errors, save form data without a prompt
451
    const { ref } = refs.getFormObjectRef.current?.() || {};
×
452
    if (ref?.current?.checkValidity() === true) {
×
453
      saveForm();
×
454
      return false;
×
455
    }
456

457
    setBlockedNavigate(true);
×
458
    return true;
×
459
  });
460

461
  const areSectionsValid = (): boolean => {
×
462
    if (status === FormStatus.LOADING) {
×
463
      return false;
×
464
    }
465

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

468
    if (!ref?.current || !newData) {
×
469
      return false;
×
470
    }
471

472
    const sectionsClone = cloneDeep(data?.questionnaireData?.sections);
×
473
    if (sectionsClone?.length !== Object.keys(map).length - 1) {
×
474
      // Not including review section
475
      return false;
×
476
    }
477

478
    const newStatus = ref.current.checkValidity() ? "Completed" : "In Progress";
×
479
    const currentSection: Section = sectionsClone.find((s) => s.name === activeSection);
×
480
    if (currentSection) {
×
481
      currentSection.status = newStatus;
×
482
    } else {
483
      sectionsClone.push({ name: activeSection, status: newStatus });
×
484
    }
485

486
    return sectionsClone?.every((section) => section.status === "Completed");
×
487
  };
488

489
  /**
490
   * Provides a save handler for the Unsaved Changes
491
   * dialog. Will save the form and then navigate to the
492
   * blocked section.
493
   *
494
   * @returns {void}
495
   */
496
  const saveAndNavigate = async () => {
×
497
    // Wait for the save handler to complete
498
    const res = await saveForm();
×
499
    const reviewSectionUrl = `/submission-request/${data["_id"]}/REVIEW`; // TODO: Update to dynamic url instead
×
500
    const isNavigatingToReviewSection = blocker?.location?.pathname === reviewSectionUrl;
×
501

502
    setBlockedNavigate(false);
×
503

504
    // if invalid data, then block navigation
505
    if (
×
506
      isNavigatingToReviewSection &&
×
507
      ((res?.status === "success" && !res?.id) || !areSectionsValid())
508
    ) {
509
      return;
×
510
    }
511

512
    blocker.proceed?.();
×
513
    if (res?.status === "success" && res.id) {
×
514
      // NOTE: This currently triggers a form data refetch, which is not ideal
515
      navigate(blocker.location.pathname.replace("new", res.id), {
×
516
        replace: true,
517
      });
518
    }
519
  };
520

521
  /**
522
   * Provides a discard handler for the Unsaved Changes
523
   * dialog. Will discard the form changes and then navigate to the
524
   * blocked section.
525
   *
526
   * @returns {void}
527
   */
528
  const discardAndNavigate = () => {
×
529
    setBlockedNavigate(false);
×
530
    blocker.proceed?.();
×
531
  };
532

533
  const handleSubmitForm = () => {
×
NEW
534
    if (!hasPermission(user, "submission_request", "submit", { data })) {
×
535
      Logger.error("Invalid request to submit Submission Request form.");
×
536
      return;
×
537
    }
538
    setOpenSubmitDialog(true);
×
539
  };
540

541
  const handleApproveForm = () => {
×
542
    if (formMode !== "Review") {
×
543
      return;
×
544
    }
545
    setOpenApproveDialog(true);
×
546
  };
547

548
  const handleInquireForm = () => {
×
549
    if (formMode !== "Review") {
×
550
      return;
×
551
    }
552
    setOpenInquireDialog(true);
×
553
  };
554

555
  const handleRejectForm = () => {
×
556
    if (formMode !== "Review") {
×
557
      return;
×
558
    }
559
    setOpenRejectDialog(true);
×
560
  };
561

562
  const handleCloseApproveFormDialog = () => {
×
563
    setOpenApproveDialog(false);
×
564
  };
565

566
  const handleCloseInquireFormDialog = () => {
×
567
    setOpenInquireDialog(false);
×
568
  };
569

570
  const handleCloseRejectFormDialog = () => {
×
571
    setOpenRejectDialog(false);
×
572
  };
573

574
  const handleBackClick = () => {
×
575
    if (status === FormStatus.SAVING || !prevSection) {
×
576
      return;
×
577
    }
578
    navigate(prevSection, { preventScrollReset: true });
×
579
  };
580

581
  const handleNextClick = () => {
×
582
    if (status === FormStatus.SAVING || !nextSection) {
×
583
      return;
×
584
    }
585
    if (isSectionD && !allSectionsComplete) {
×
586
      return;
×
587
    }
588
    navigate(nextSection, { preventScrollReset: true });
×
589
  };
590

591
  // Intercept browser navigation actions (e.g. closing the tab) with unsaved changes
592
  useEffect(() => {
×
593
    const unloadHandler = (event: BeforeUnloadEvent) => {
×
594
      if (!isDirty()) {
×
595
        return;
×
596
      }
597

598
      // If there are no validation errors, save form data without a prompt
599
      const { ref } = refs.getFormObjectRef.current?.() || {};
×
600
      if (ref?.current?.checkValidity() === true) {
×
601
        saveForm();
×
602
        return;
×
603
      }
604

605
      event.preventDefault();
×
606
      event.returnValue = "You have unsaved form changes. Are you sure you want to leave?";
×
607
    };
608

609
    window.addEventListener("beforeunload", unloadHandler);
×
610

611
    return () => {
×
612
      window.removeEventListener("beforeunload", unloadHandler);
×
613
    };
614
  });
615

616
  useEffect(() => {
×
617
    const isComplete = isAllSectionsComplete();
×
618
    setAllSectionsComplete(isComplete);
×
619
  }, [status, data]);
620

621
  useEffect(() => {
×
622
    if (status !== FormStatus.LOADED && authStatus !== AuthStatus.LOADED) {
×
623
      return;
×
624
    }
625
    if (!hasReopenedFormRef.current && data?.status === "Inquired" && formMode === "Edit") {
×
626
      handleReopenForm();
×
627
      hasReopenedFormRef.current = true;
×
628
    }
629
  }, [status, authStatus, formMode, data?.status]);
630

631
  // Show loading spinner if the form is still loading
632
  if (status === FormStatus.LOADING || authStatus === AuthStatus.LOADING) {
×
633
    return <SuspenseLoader />;
×
634
  }
635

636
  // Hide form content if the user is unauthorized
637
  if (authStatus === AuthStatus.ERROR) {
×
638
    return null;
×
639
  }
640

641
  // Redirect to ListView if no data is found and the form is in the error state
642
  if (status === FormStatus.ERROR && !data?._id) {
×
643
    return <Navigate to="/submission-requests" state={{ error: error || "Unknown error" }} />;
×
644
  }
645

646
  return (
×
647
    <>
648
      <PageBanner
649
        title="Submission Request Form"
650
        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."
651
        bannerSrc={bannerPng}
652
      />
653

654
      <StyledContainer ref={formContentRef} maxWidth="xl">
655
        <StyledContentWrapper direction="row" justifyContent="center">
656
          <StyledSidebar direction="row" justifyContent="center" alignSelf="flex-start">
657
            <ProgressBar section={activeSection} />
658
            <StyledDivider orientation="vertical" />
659
          </StyledSidebar>
660

661
          <StyledContent direction="column" spacing={5}>
662
            <StatusBar />
663

664
            <Section section={activeSection} refs={refs} />
665

666
            <StyledControls direction="row" justifyContent="center" alignItems="center" spacing={2}>
667
              <StyledLoadingButton
668
                id="submission-form-back-button"
669
                variant="contained"
670
                color="info"
671
                type="button"
672
                disabled={status === FormStatus.SAVING || !prevSection}
×
673
                onClick={handleBackClick}
674
                size="large"
675
                startIcon={<ChevronLeft />}
676
              >
677
                Back
678
              </StyledLoadingButton>
679
              {activeSection !== "REVIEW" && formMode === "Edit" && (
×
680
                <StyledLoadingButton
681
                  id="submission-form-save-button"
682
                  variant="contained"
683
                  color="success"
684
                  loading={status === FormStatus.SAVING}
685
                  onClick={() => saveForm()}
×
686
                >
687
                  Save
688
                </StyledLoadingButton>
689
              )}
690
              {activeSection !== "REVIEW" && (
×
691
                <StyledLoadingButton
692
                  id="submission-form-next-button"
693
                  variant="contained"
694
                  color="info"
695
                  type="button"
696
                  onClick={handleNextClick}
697
                  disabled={
698
                    status === FormStatus.SAVING ||
×
699
                    !nextSection ||
700
                    (isSectionD && !allSectionsComplete)
701
                  }
702
                  size="large"
703
                  endIcon={<ChevronRight />}
704
                >
705
                  Next
706
                </StyledLoadingButton>
707
              )}
708

709
              {activeSection === "REVIEW" &&
×
710
                hasPermission(user, "submission_request", "submit", { data }) && (
711
                  <StyledExtendedLoadingButton
712
                    id="submission-form-submit-button"
713
                    variant="contained"
714
                    color="primary"
715
                    type="submit"
716
                    size="large"
717
                    onClick={handleSubmitForm}
718
                  >
719
                    Submit
720
                  </StyledExtendedLoadingButton>
721
                )}
722

723
              {activeSection === "REVIEW" && formMode === "Review" && (
×
724
                <>
725
                  <StyledExtendedLoadingButton
726
                    id="submission-form-approve-button"
727
                    variant="contained"
728
                    color="primary"
729
                    size="large"
730
                    onClick={handleApproveForm}
731
                  >
732
                    Approve
733
                  </StyledExtendedLoadingButton>
734
                  <StyledLoadingButton
735
                    id="submission-form-inquire-button"
736
                    variant="contained"
737
                    color="error"
738
                    size="large"
739
                    onClick={handleInquireForm}
740
                  >
741
                    Request Additional Information
742
                  </StyledLoadingButton>
743
                  <StyledExtendedLoadingButton
744
                    id="submission-form-reject-button"
745
                    variant="contained"
746
                    color="error"
747
                    size="large"
748
                    onClick={handleRejectForm}
749
                  >
750
                    Reject
751
                  </StyledExtendedLoadingButton>
752
                </>
753
              )}
754

755
              {activeSection === "REVIEW" && (
×
756
                <ExportRequestButton disabled={!allSectionsComplete} />
757
              )}
758
            </StyledControls>
759
          </StyledContent>
760
        </StyledContentWrapper>
761
      </StyledContainer>
762

763
      <UnsavedChangesDialog
764
        open={blockedNavigate}
765
        onCancel={() => setBlockedNavigate(false)}
×
766
        onSave={saveAndNavigate}
767
        onDiscard={discardAndNavigate}
768
        disableActions={status === FormStatus.SAVING}
769
      />
770
      <SubmitFormDialog
771
        open={openSubmitDialog}
772
        onCancel={() => setOpenSubmitDialog(false)}
×
773
        onSubmit={submitForm}
774
        disableActions={status === FormStatus.SUBMITTING}
775
      />
776
      <ApproveFormDialog
777
        open={openApproveDialog}
778
        onCancel={handleCloseApproveFormDialog}
779
        onSubmit={(reviewComment) => submitApproveForm(reviewComment)}
×
780
      />
781
      <InquireFormDialog
782
        open={openInquireDialog}
783
        onCancel={handleCloseInquireFormDialog}
784
        onSubmit={(reviewComment) => submitInquireForm(reviewComment)}
×
785
      />
786
      <RejectFormDialog
787
        open={openRejectDialog}
788
        onCancel={handleCloseRejectFormDialog}
789
        onSubmit={(reviewComment) => submitRejectForm(reviewComment)}
×
790
      />
791
    </>
792
  );
793
};
794

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

© 2025 Coveralls, Inc