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

CBIIT / crdc-datahub-ui / 14406039087

11 Apr 2025 02:58PM UTC coverage: 62.222% (+0.01%) from 62.208%
14406039087

push

github

web-flow
Merge pull request #662 from CBIIT/CRDCDH-2477

CRDCDH-2477 Request Access with Institution selection

3366 of 5797 branches covered (58.06%)

Branch coverage included in aggregate %.

9 of 10 new or added lines in 3 files covered. (90.0%)

1 existing line in 1 file now uncovered.

4655 of 7094 relevant lines covered (65.62%)

183.85 hits per line

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

82.98
/src/components/AccessRequest/FormDialog.tsx
1
import React, { FC } from "react";
2
import { Box, DialogProps, MenuItem, styled, TextField } from "@mui/material";
3
import { Controller, SubmitHandler, useForm } from "react-hook-form";
4
import { LoadingButton } from "@mui/lab";
5
import { useMutation, useQuery } from "@apollo/client";
6
import { useSnackbar } from "notistack";
7
import { ReactComponent as CloseIconSvg } from "../../assets/icons/close_icon.svg";
8
import StyledOutlinedInput from "../StyledFormComponents/StyledOutlinedInput";
9
import StyledLabel from "../StyledFormComponents/StyledLabel";
10
import StyledAsterisk from "../StyledFormComponents/StyledAsterisk";
11
import StyledHelperText from "../StyledFormComponents/StyledHelperText";
12
import StyledCloseDialogButton from "../StyledDialogComponents/StyledDialogCloseButton";
13
import DefaultDialog from "../StyledDialogComponents/StyledDialog";
14
import StyledDialogContent from "../StyledDialogComponents/StyledDialogContent";
15
import DefaultDialogHeader from "../StyledDialogComponents/StyledHeader";
16
import StyledBodyText from "../StyledDialogComponents/StyledBodyText";
17
import DefaultDialogActions from "../StyledDialogComponents/StyledDialogActions";
18
import StyledSelect from "../StyledFormComponents/StyledSelect";
19
import { useAuthContext } from "../Contexts/AuthContext";
20
import { useInstitutionList } from "../Contexts/InstitutionListContext";
21
import {
22
  LIST_APPROVED_STUDIES,
23
  ListApprovedStudiesInput,
24
  ListApprovedStudiesResp,
25
  REQUEST_ACCESS,
26
  RequestAccessInput,
27
  RequestAccessResp,
28
} from "../../graphql";
29
import { Logger } from "../../utils";
30
import StyledAutocomplete from "../StyledFormComponents/StyledAutocomplete";
31

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

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

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

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

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

66
export type InputForm = RequestAccessInput;
67

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

72
const RoleOptions: UserRole[] = ["Submitter"];
4✔
73

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

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

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

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

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

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

133
    onClose();
2✔
134
  };
135

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

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