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

CBIIT / crdc-datahub-ui / 11598764483

30 Oct 2024 05:43PM UTC coverage: 53.681% (+0.2%) from 53.443%
11598764483

push

github

web-flow
Merge pull request #499 from CBIIT/CRDCDH-1743

CRDCDH-1743 Implement "Request Access" form

2557 of 5259 branches covered (48.62%)

Branch coverage included in aggregate %.

52 of 66 new or added lines in 14 files covered. (78.79%)

2 existing lines in 2 files now uncovered.

3692 of 6382 relevant lines covered (57.85%)

130.88 hits per line

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

82.0
/src/components/AccessRequest/FormDialog.tsx
1
import React, { FC, useMemo } 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 { cloneDeep } from "lodash";
7
import { useSnackbar } from "notistack";
8
import { ReactComponent as CloseIconSvg } from "../../assets/icons/close_icon.svg";
9
import StyledOutlinedInput from "../StyledFormComponents/StyledOutlinedInput";
10
import StyledLabel from "../StyledFormComponents/StyledLabel";
11
import StyledAsterisk from "../StyledFormComponents/StyledAsterisk";
12
import Tooltip from "../Tooltip";
13
import StyledHelperText from "../StyledFormComponents/StyledHelperText";
14
import StyledCloseDialogButton from "../StyledDialogComponents/StyledDialogCloseButton";
15
import DefaultDialog from "../StyledDialogComponents/StyledDialog";
16
import StyledDialogContent from "../StyledDialogComponents/StyledDialogContent";
17
import DefaultDialogHeader from "../StyledDialogComponents/StyledHeader";
18
import StyledBodyText from "../StyledDialogComponents/StyledBodyText";
19
import DefaultDialogActions from "../StyledDialogComponents/StyledDialogActions";
20
import StyledSelect from "../StyledFormComponents/StyledSelect";
21
import { useAuthContext } from "../Contexts/AuthContext";
22
import StyledAutocomplete from "../StyledFormComponents/StyledAutocomplete";
23
import {
24
  LIST_ORG_NAMES,
25
  ListOrgNamesResp,
26
  REQUEST_ACCESS,
27
  RequestAccessInput,
28
  RequestAccessResp,
29
} from "../../graphql";
30
import { Logger } from "../../utils";
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 = {
67
  role: UserRole;
68
  organization: string;
69
  additionalInfo: string;
70
};
71

72
type Props = {
73
  onClose: () => void;
74
} & Omit<DialogProps, "onClose">;
75

76
const RoleOptions: UserRole[] = ["Submitter", "Organization Owner"];
4✔
77

78
/**
79
 * Provides a dialog for users to request access to a specific role.
80
 *
81
 * @param {Props} props
82
 * @returns {React.FC<Props>}
83
 */
84
const FormDialog: FC<Props> = ({ onClose, ...rest }) => {
4✔
85
  const { user } = useAuthContext();
60✔
86
  const { enqueueSnackbar } = useSnackbar();
60✔
87

88
  const { handleSubmit, register, control, formState } = useForm<InputForm>({
60✔
89
    defaultValues: {
90
      role: RoleOptions.includes(user.role) ? user.role : "Submitter",
30✔
91
      organization: user?.organization?.orgName || "",
51✔
92
    },
93
  });
94
  const { errors, isSubmitting } = formState;
60✔
95

96
  const { data } = useQuery<ListOrgNamesResp>(LIST_ORG_NAMES, {
60✔
97
    context: { clientName: "backend" },
98
    fetchPolicy: "cache-first",
99
    onError: () => {
100
      enqueueSnackbar("Unable to retrieve organization list.", {
4✔
101
        variant: "error",
102
      });
103
    },
104
  });
105

106
  const [requestAccess] = useMutation<RequestAccessResp, RequestAccessInput>(REQUEST_ACCESS, {
60✔
107
    context: { clientName: "backend" },
108
    fetchPolicy: "no-cache",
109
  });
110

111
  const sortedOrgs = useMemo<string[]>(
60✔
112
    () =>
113
      cloneDeep(data?.listOrganizations)
34✔
NEW
114
        ?.map(({ name }) => name)
×
NEW
115
        ?.sort((a, b) => a.localeCompare(b)) || [],
×
116
    [data]
117
  );
118

119
  const onSubmit: SubmitHandler<InputForm> = async ({
60✔
120
    role,
121
    organization,
122
    additionalInfo,
123
  }: InputForm) => {
124
    const { data, errors } = await requestAccess({
10✔
125
      variables: {
126
        role,
127
        organization: organization?.trim(),
128
        additionalInfo,
129
      },
NEW
130
    }).catch((e) => ({
×
131
      data: null,
132
      errors: e,
133
    }));
134

135
    if (!data?.requestAccess?.success || errors) {
10!
NEW
136
      enqueueSnackbar("Unable to submit access request form. Please try again.", {
×
137
        variant: "error",
138
      });
NEW
139
      Logger.error("Unable to submit form", errors);
×
NEW
140
      return;
×
141
    }
142

143
    onClose();
10✔
144
  };
145

146
  return (
60✔
147
    <StyledDialog
148
      onClose={onClose}
149
      aria-labelledby="access-request-dialog-header"
150
      data-testid="access-request-dialog"
151
      scroll="body"
152
      {...rest}
153
    >
154
      <StyledCloseDialogButton
155
        data-testid="access-request-dialog-close-icon"
156
        aria-label="close"
157
        onClick={onClose}
158
      >
159
        <CloseIconSvg />
160
      </StyledCloseDialogButton>
161
      <StyledHeader
162
        id="access-request-dialog-header"
163
        data-testid="access-request-dialog-header"
164
        variant="h1"
165
      >
166
        Request Access
167
      </StyledHeader>
168
      <StyledDialogContent>
169
        <StyledBodyText data-testid="access-request-dialog-body" variant="body1">
170
          Please fill out the form below to request access.
171
        </StyledBodyText>
172
        <StyledForm>
173
          <Box>
174
            <StyledLabel id="role-input-label">
175
              Role
176
              <StyledAsterisk />
177
            </StyledLabel>
178
            <Controller
179
              name="role"
180
              control={control}
181
              rules={{ required: "This field is required" }}
182
              render={({ field }) => (
183
                <StyledSelect
60✔
184
                  {...field}
185
                  size="small"
186
                  MenuProps={{ disablePortal: true }}
187
                  data-testid="access-request-role-field"
188
                  inputProps={{ "aria-labelledby": "role-input-label" }}
189
                >
190
                  {RoleOptions.map((role) => (
191
                    <MenuItem key={role} value={role}>
120✔
192
                      {role}
193
                    </MenuItem>
194
                  ))}
195
                </StyledSelect>
196
              )}
197
            />
198
            <StyledHelperText data-testid="access-request-dialog-error-role">
199
              {errors?.role?.message}
200
            </StyledHelperText>
201
          </Box>
202
          <Box>
203
            <StyledLabel id="organization-input-label">
204
              Organization
205
              <StyledAsterisk />
206
            </StyledLabel>
207
            <Controller
208
              name="organization"
209
              control={control}
210
              rules={{ required: "This field is required" }}
211
              render={({ field }) => (
212
                <StyledAutocomplete
136✔
213
                  {...field}
214
                  options={sortedOrgs}
NEW
215
                  onChange={(_, data: string) => field.onChange(data.trim())}
×
216
                  onInputChange={(_, data: string) => field.onChange(data.trim())}
106✔
217
                  renderInput={({ inputProps, ...params }) => (
218
                    <TextField
204✔
219
                      {...params}
220
                      inputProps={{ "aria-labelledby": "organization-input-label", ...inputProps }}
221
                      placeholder="Enter your organization or Select one from the list"
222
                    />
223
                  )}
224
                  data-testid="access-request-organization-field"
225
                  freeSolo
226
                />
227
              )}
228
            />
229
            <StyledHelperText data-testid="access-request-dialog-error-organization">
230
              {errors?.organization?.message}
231
            </StyledHelperText>
232
          </Box>
233
          <Box>
234
            <StyledLabel id="additionalInfo-input-label">
235
              Additional Info
236
              <Tooltip
237
                title="Provide details such as your host institution or lab, along with the study or program you are submitting data for, to help us determine your associated organization."
238
                open={undefined}
239
                disableHoverListener={false}
240
                data-testid="additionalInfo-input-tooltip"
241
              />
242
            </StyledLabel>
243
            <StyledOutlinedInput
244
              {...register("additionalInfo", {
245
                setValueAs: (v: string) => v?.trim(),
468✔
246
                validate: {
247
                  maxLength: (v: string) =>
248
                    v.length > 200 ? "Maximum of 200 characters allowed" : null,
12!
249
                },
250
              })}
251
              placeholder="Maximum of 200 characters"
252
              data-testid="access-request-additionalInfo-field"
253
              inputProps={{ "aria-labelledby": "additionalInfo-input-label", maxLength: 200 }}
254
              multiline
255
              rows={3}
256
            />
257
            <StyledHelperText data-testid="access-request-dialog-error-additionalInfo">
258
              {errors?.additionalInfo?.message}
259
            </StyledHelperText>
260
          </Box>
261
        </StyledForm>
262
      </StyledDialogContent>
263
      <StyledDialogActions>
264
        <StyledButton
265
          data-testid="access-request-dialog-cancel-button"
266
          variant="contained"
267
          color="info"
268
          size="large"
269
          onClick={onClose}
270
        >
271
          Cancel
272
        </StyledButton>
273
        <StyledButton
274
          data-testid="access-request-dialog-submit-button"
275
          variant="contained"
276
          color="success"
277
          size="large"
278
          onClick={handleSubmit(onSubmit)}
279
          loading={isSubmitting}
280
        >
281
          Submit
282
        </StyledButton>
283
      </StyledDialogActions>
284
    </StyledDialog>
285
  );
286
};
287

288
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