• 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

93.71
/src/components/AccessRequest/FormDialog.tsx
1
import { useMutation, useQuery } from "@apollo/client";
1✔
2
import { LoadingButton } from "@mui/lab";
1✔
3
import { Box, DialogProps, MenuItem, styled, TextField } from "@mui/material";
1✔
4
import { useSnackbar } from "notistack";
1✔
5
import React, { FC } from "react";
1✔
6
import { Controller, SubmitHandler, useForm } from "react-hook-form";
1✔
7

8
import CloseIconSvg from "../../assets/icons/close_icon.svg?react";
1✔
9
import {
1✔
10
  LIST_APPROVED_STUDIES,
11
  ListApprovedStudiesInput,
12
  ListApprovedStudiesResp,
13
  REQUEST_ACCESS,
14
  RequestAccessInput,
15
  RequestAccessResp,
16
} from "../../graphql";
17
import { formatFullStudyName, Logger, validateUTF8 } from "../../utils";
1✔
18
import { useAuthContext } from "../Contexts/AuthContext";
1✔
19
import { useInstitutionList } from "../Contexts/InstitutionListContext";
1✔
20
import StyledBodyText from "../StyledDialogComponents/StyledBodyText";
1✔
21
import DefaultDialog from "../StyledDialogComponents/StyledDialog";
1✔
22
import DefaultDialogActions from "../StyledDialogComponents/StyledDialogActions";
1✔
23
import StyledCloseDialogButton from "../StyledDialogComponents/StyledDialogCloseButton";
1✔
24
import StyledDialogContent from "../StyledDialogComponents/StyledDialogContent";
1✔
25
import DefaultDialogHeader from "../StyledDialogComponents/StyledHeader";
1✔
26
import StyledAsterisk from "../StyledFormComponents/StyledAsterisk";
1✔
27
import StyledAutocomplete from "../StyledFormComponents/StyledAutocomplete";
1✔
28
import StyledHelperText from "../StyledFormComponents/StyledHelperText";
1✔
29
import StyledLabel from "../StyledFormComponents/StyledLabel";
1✔
30
import StyledOutlinedInput from "../StyledFormComponents/StyledOutlinedInput";
1✔
31
import StyledSelect from "../StyledFormComponents/StyledSelect";
1✔
32

33
const StyledDialog = styled(DefaultDialog)({
1✔
34
  "& .MuiDialog-paper": {
1✔
35
    width: "803px !important",
1✔
36
    border: "2px solid #5AB8FF",
1✔
37
  },
1✔
38
});
1✔
39

40
const StyledForm = styled("form")({
1✔
41
  display: "flex",
1✔
42
  flexDirection: "column",
1✔
43
  gap: "8px",
1✔
44
  margin: "0 auto",
1✔
45
  marginTop: "28px",
1✔
46
  maxWidth: "485px",
1✔
47
});
1✔
48

49
const StyledHeader = styled(DefaultDialogHeader)({
1✔
50
  color: "#1873BD",
1✔
51
  fontSize: "45px !important",
1✔
52
  marginBottom: "24px !important",
1✔
53
});
1✔
54

55
const StyledDialogActions = styled(DefaultDialogActions)({
1✔
56
  marginTop: "36px !important",
1✔
57
});
1✔
58

59
const StyledButton = styled(LoadingButton)({
1✔
60
  minWidth: "137px",
1✔
61
  padding: "10px",
1✔
62
  fontSize: "16px",
1✔
63
  lineHeight: "24px",
1✔
64
  letterSpacing: "0.32px",
1✔
65
});
1✔
66

67
export type InputForm = RequestAccessInput;
68

69
type Props = {
70
  onClose: () => void;
71
} & Omit<DialogProps, "onClose">;
72

73
const RoleOptions: UserRole[] = ["Submitter"];
1✔
74

75
/**
76
 * Provides a dialog for users to request access to a specific role.
77
 *
78
 * @param {Props} props
79
 * @returns {React.FC<Props>}
80
 */
81
const FormDialog: FC<Props> = ({ onClose, ...rest }) => {
1✔
82
  const { user } = useAuthContext();
17✔
83
  const { enqueueSnackbar } = useSnackbar();
17✔
84
  const { data: listInstitutions } = useInstitutionList();
17✔
85

86
  const { handleSubmit, register, control, formState } = useForm<InputForm>({
17✔
87
    defaultValues: {
17✔
88
      role: RoleOptions.includes(user.role) ? user.role : "Submitter",
17!
89
      institutionName: "",
17✔
90
      studies: [],
17✔
91
      additionalInfo: "",
17✔
92
    },
17✔
93
  });
17✔
94
  const { errors, isSubmitting } = formState;
17✔
95

96
  const { data: listStudies } = useQuery<ListApprovedStudiesResp, ListApprovedStudiesInput>(
17✔
97
    LIST_APPROVED_STUDIES,
17✔
98
    {
17✔
99
      variables: {
17✔
100
        orderBy: "studyName",
17✔
101
        sortDirection: "asc",
17✔
102
      },
17✔
103
      context: { clientName: "backend" },
17✔
104
      fetchPolicy: "cache-first",
17✔
105
      onError: () => {
17✔
106
        enqueueSnackbar("Unable to retrieve approved studies list.", {
2✔
107
          variant: "error",
2✔
108
        });
2✔
109
      },
2✔
110
    }
17✔
111
  );
17✔
112

113
  const [requestAccess] = useMutation<RequestAccessResp, RequestAccessInput>(REQUEST_ACCESS, {
17✔
114
    context: { clientName: "backend" },
17✔
115
    fetchPolicy: "no-cache",
17✔
116
  });
17✔
117

118
  const onSubmit: SubmitHandler<InputForm> = async (input: InputForm) => {
17✔
119
    const { data, errors } = await requestAccess({
1✔
120
      variables: input,
1✔
121
    }).catch((e) => ({
1✔
UNCOV
122
      data: null,
×
UNCOV
123
      errors: e,
×
124
    }));
1✔
125

126
    if (!data?.requestAccess?.success || errors) {
1!
127
      enqueueSnackbar("Unable to submit access request form. Please try again.", {
×
UNCOV
128
        variant: "error",
×
UNCOV
129
      });
×
130
      Logger.error("Unable to submit form", errors);
×
131
      return;
×
UNCOV
132
    }
×
133

134
    onClose();
1✔
135
  };
1✔
136

137
  return (
17✔
138
    <StyledDialog
17✔
139
      onClose={onClose}
17✔
140
      aria-labelledby="access-request-dialog-header"
17✔
141
      data-testid="access-request-dialog"
17✔
142
      scroll="body"
17✔
143
      {...rest}
17✔
144
    >
145
      <StyledCloseDialogButton
17✔
146
        data-testid="access-request-dialog-close-icon"
17✔
147
        aria-label="close"
17✔
148
        onClick={onClose}
17✔
149
      >
150
        <CloseIconSvg />
17✔
151
      </StyledCloseDialogButton>
17✔
152
      <StyledHeader
17✔
153
        id="access-request-dialog-header"
17✔
154
        data-testid="access-request-dialog-header"
17✔
155
        variant="h1"
17✔
156
      >
157
        Request Access
158
      </StyledHeader>
17✔
159
      <StyledDialogContent>
17✔
160
        <StyledBodyText data-testid="access-request-dialog-body" variant="body1">
17✔
161
          Please fill out the form below to request access.
162
        </StyledBodyText>
17✔
163
        <StyledForm>
17✔
164
          <Box>
17✔
165
            <StyledLabel id="role-input-label">
17✔
166
              Role
167
              <StyledAsterisk />
17✔
168
            </StyledLabel>
17✔
169
            <Controller
17✔
170
              name="role"
17✔
171
              control={control}
17✔
172
              rules={{ required: "This field is required" }}
17✔
173
              render={({ field }) => (
17✔
174
                <StyledSelect
17✔
175
                  {...field}
17✔
176
                  size="small"
17✔
177
                  MenuProps={{ disablePortal: true }}
17✔
178
                  data-testid="access-request-role-field"
17✔
179
                  inputProps={{ "aria-labelledby": "role-input-label" }}
17✔
180
                >
181
                  {RoleOptions.map((role) => (
17✔
182
                    <MenuItem key={role} value={role}>
17✔
183
                      {role}
17✔
184
                    </MenuItem>
17✔
185
                  ))}
17✔
186
                </StyledSelect>
17✔
187
              )}
17✔
188
            />
189
            <StyledHelperText data-testid="access-request-dialog-error-role">
17✔
190
              {errors?.role?.message}
17!
191
            </StyledHelperText>
17✔
192
          </Box>
17✔
193
          <Box>
17✔
194
            <StyledLabel id="institution-input-label">
17✔
195
              Institution
196
              <StyledAsterisk />
17✔
197
            </StyledLabel>
17✔
198
            <Controller
17✔
199
              name="institutionName"
17✔
200
              control={control}
17✔
201
              rules={{
17✔
202
                validate: {
17✔
203
                  required: (v: string) => v.trim() !== "" || "This field is required",
17!
204
                  maxLength: (v: string) => v.length <= 100 || "Maximum of 100 characters allowed",
17!
205
                  utf8: validateUTF8,
17✔
206
                },
17✔
207
              }}
17✔
208
              render={({ field }) => (
17✔
209
                <StyledAutocomplete
27✔
210
                  {...field}
27✔
211
                  options={listInstitutions?.map((i) => i.name) || []}
27!
212
                  onChange={(_, data: string) => field.onChange(data.trim())}
27✔
213
                  onInputChange={(_, data: string) => field.onChange(data.trim())}
27✔
214
                  renderInput={({ inputProps, ...params }) => (
27✔
215
                    <TextField
38✔
216
                      {...params}
38✔
217
                      inputProps={{
38✔
218
                        "aria-labelledby": "institution-input-label",
38✔
219
                        maxLength: 100,
38✔
220
                        ...inputProps,
38✔
221
                      }}
38✔
222
                      placeholder="100 characters allowed"
38✔
223
                    />
224
                  )}
225
                  data-testid="access-request-institution-field"
27✔
226
                  freeSolo
27✔
227
                />
228
              )}
17✔
229
            />
230
            <StyledHelperText data-testid="access-request-dialog-error-institution">
17✔
231
              {errors?.institutionName?.message}
17!
232
            </StyledHelperText>
17✔
233
          </Box>
17✔
234
          <Box>
17✔
235
            <StyledLabel id="studies-input-label">
17✔
236
              Studies
237
              <StyledAsterisk />
17✔
238
            </StyledLabel>
17✔
239
            <Controller
17✔
240
              name="studies"
17✔
241
              control={control}
17✔
242
              rules={{ required: "This field is required" }}
17✔
243
              render={({ field }) => (
17✔
244
                <StyledSelect
18✔
245
                  {...field}
18✔
246
                  size="small"
18✔
247
                  MenuProps={{ disablePortal: true }}
18✔
248
                  data-testid="access-request-studies-field"
18✔
249
                  inputProps={{
18✔
250
                    "aria-labelledby": "studies-input-label",
18✔
251
                  }}
18✔
252
                  placeholderText="Select one or more studies from the list"
18✔
253
                  multiple
18✔
254
                >
255
                  {listStudies?.listApprovedStudies?.studies?.map((study) => (
18✔
256
                    <MenuItem
12✔
257
                      key={study._id}
12✔
258
                      value={study._id}
12✔
259
                      data-testid={`studies-${study.studyName}`}
12✔
260
                    >
261
                      {formatFullStudyName(study.studyName, study.studyAbbreviation)}
12✔
262
                    </MenuItem>
12✔
263
                  ))}
18✔
264
                </StyledSelect>
18✔
265
              )}
17✔
266
            />
267
            <StyledHelperText data-testid="access-request-dialog-error-organization">
17✔
268
              {errors?.studies?.message}
17!
269
            </StyledHelperText>
17✔
270
          </Box>
17✔
271
          <Box>
17✔
272
            <StyledLabel id="additionalInfo-input-label">Additional Info</StyledLabel>
17✔
273
            <StyledOutlinedInput
17✔
274
              {...register("additionalInfo", {
17✔
275
                setValueAs: (v: string) => v?.trim(),
17✔
276
                validate: {
17✔
277
                  maxLength: (v: string) =>
17✔
278
                    v.length > 200 ? "Maximum of 200 characters allowed" : null,
1!
279
                },
17✔
280
              })}
17✔
281
              placeholder="Maximum of 200 characters"
17✔
282
              data-testid="access-request-additionalInfo-field"
17✔
283
              inputProps={{ "aria-labelledby": "additionalInfo-input-label", maxLength: 200 }}
17✔
284
              multiline
17✔
285
              rows={3}
17✔
286
            />
287
            <StyledHelperText data-testid="access-request-dialog-error-additionalInfo">
17✔
288
              {errors?.additionalInfo?.message}
17!
289
            </StyledHelperText>
17✔
290
          </Box>
17✔
291
        </StyledForm>
17✔
292
      </StyledDialogContent>
17✔
293
      <StyledDialogActions>
17✔
294
        <StyledButton
17✔
295
          data-testid="access-request-dialog-cancel-button"
17✔
296
          variant="contained"
17✔
297
          color="info"
17✔
298
          size="large"
17✔
299
          onClick={onClose}
17✔
300
        >
301
          Cancel
302
        </StyledButton>
17✔
303
        <StyledButton
17✔
304
          data-testid="access-request-dialog-submit-button"
17✔
305
          variant="contained"
17✔
306
          color="success"
17✔
307
          size="large"
17✔
308
          onClick={handleSubmit(onSubmit)}
17✔
309
          loading={isSubmitting}
17✔
310
        >
311
          Submit
312
        </StyledButton>
17✔
313
      </StyledDialogActions>
17✔
314
    </StyledDialog>
17✔
315
  );
316
};
17✔
317

318
export default React.memo<Props>(FormDialog);
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