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

CBIIT / crdc-datahub-ui / 15116039371

19 May 2025 02:43PM UTC coverage: 62.165% (-0.05%) from 62.21%
15116039371

Pull #717

github

web-flow
Merge 5d92b201c into 9980b5539
Pull Request #717: CRDCDH-2743 Standardize Study Display

3472 of 6004 branches covered (57.83%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

4804 of 7309 relevant lines covered (65.73%)

216.73 hits per line

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

70.91
/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 { formatFullStudyName, Logger, validateUTF8 } 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();
30✔
82
  const { enqueueSnackbar } = useSnackbar();
30✔
83
  const { data: listInstitutions } = useInstitutionList();
30✔
84

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

95
  const { data: listStudies } = useQuery<ListApprovedStudiesResp, ListApprovedStudiesInput>(
30✔
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, {
30✔
113
    context: { clientName: "backend" },
114
    fetchPolicy: "no-cache",
115
  });
116

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

UNCOV
125
    if (!data?.requestAccess?.success || errors) {
×
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

UNCOV
133
    onClose();
×
134
  };
135

136
  return (
30✔
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
30✔
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}>
30✔
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={{
201
                validate: {
202
                  required: (v: string) => v.trim() !== "" || "This field is required",
2!
203
                  maxLength: (v: string) => v.length <= 100 || "Maximum of 100 characters allowed",
2!
204
                  utf8: validateUTF8,
205
                },
206
              }}
207
              render={({ field }) => (
208
                <StyledAutocomplete
50✔
209
                  {...field}
210
                  options={listInstitutions?.map((i) => i.name) || []}
84!
211
                  onChange={(_, data: string) => field.onChange(data.trim())}
×
212
                  onInputChange={(_, data: string) => field.onChange(data.trim())}
20✔
213
                  renderInput={({ inputProps, ...params }) => (
214
                    <TextField
72✔
215
                      {...params}
216
                      inputProps={{
217
                        "aria-labelledby": "institution-input-label",
218
                        maxLength: 100,
219
                        ...inputProps,
220
                      }}
221
                      placeholder="100 characters allowed"
222
                    />
223
                  )}
224
                  data-testid="access-request-institution-field"
225
                  freeSolo
226
                />
227
              )}
228
            />
229
            <StyledHelperText data-testid="access-request-dialog-error-institution">
230
              {errors?.institutionName?.message}
231
            </StyledHelperText>
232
          </Box>
233
          <Box>
234
            <StyledLabel id="studies-input-label">
235
              Studies
236
              <StyledAsterisk />
237
            </StyledLabel>
238
            <Controller
239
              name="studies"
240
              control={control}
241
              rules={{ required: "This field is required" }}
242
              render={({ field }) => (
243
                <StyledSelect
32✔
244
                  {...field}
245
                  size="small"
246
                  MenuProps={{ disablePortal: true }}
247
                  data-testid="access-request-studies-field"
248
                  inputProps={{
249
                    "aria-labelledby": "studies-input-label",
250
                  }}
251
                  placeholderText="Select one or more studies from the list"
252
                  multiple
253
                >
254
                  {listStudies?.listApprovedStudies?.studies?.map((study) => (
255
                    <MenuItem
16✔
256
                      key={study._id}
257
                      value={study._id}
258
                      data-testid={`studies-${study.studyName}`}
259
                    >
260
                      {formatFullStudyName(study.studyName, study.studyAbbreviation)}
261
                    </MenuItem>
262
                  ))}
263
                </StyledSelect>
264
              )}
265
            />
266
            <StyledHelperText data-testid="access-request-dialog-error-organization">
267
              {errors?.studies?.message}
268
            </StyledHelperText>
269
          </Box>
270
          <Box>
271
            <StyledLabel id="additionalInfo-input-label">Additional Info</StyledLabel>
272
            <StyledOutlinedInput
273
              {...register("additionalInfo", {
274
                setValueAs: (v: string) => v?.trim(),
18✔
275
                validate: {
276
                  maxLength: (v: string) =>
277
                    v.length > 200 ? "Maximum of 200 characters allowed" : null,
2!
278
                },
279
              })}
280
              placeholder="Maximum of 200 characters"
281
              data-testid="access-request-additionalInfo-field"
282
              inputProps={{ "aria-labelledby": "additionalInfo-input-label", maxLength: 200 }}
283
              multiline
284
              rows={3}
285
            />
286
            <StyledHelperText data-testid="access-request-dialog-error-additionalInfo">
287
              {errors?.additionalInfo?.message}
288
            </StyledHelperText>
289
          </Box>
290
        </StyledForm>
291
      </StyledDialogContent>
292
      <StyledDialogActions>
293
        <StyledButton
294
          data-testid="access-request-dialog-cancel-button"
295
          variant="contained"
296
          color="info"
297
          size="large"
298
          onClick={onClose}
299
        >
300
          Cancel
301
        </StyledButton>
302
        <StyledButton
303
          data-testid="access-request-dialog-submit-button"
304
          variant="contained"
305
          color="success"
306
          size="large"
307
          onClick={handleSubmit(onSubmit)}
308
          loading={isSubmitting}
309
        >
310
          Submit
311
        </StyledButton>
312
      </StyledDialogActions>
313
    </StyledDialog>
314
  );
315
};
316

317
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

© 2025 Coveralls, Inc