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

CBIIT / crdc-datahub-ui / 17132131774

21 Aug 2025 03:52PM UTC coverage: 77.592% (+1.7%) from 75.941%
17132131774

Pull #806

github

web-flow
Merge 6b88b37d9 into c10ceac73
Pull Request #806: Submission Request Excel Import & Export CRDCDH-3033, CRDCDH-3045, CRDCDH-3063

4841 of 5322 branches covered (90.96%)

Branch coverage included in aggregate %.

3122 of 3394 new or added lines in 32 files covered. (91.99%)

7 existing lines in 3 files now uncovered.

28996 of 38287 relevant lines covered (75.73%)

1856.98 hits per line

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

71.83
/src/components/Contexts/FormContext.tsx
1
import { useLazyQuery, useMutation } from "@apollo/client";
1✔
2
import { merge, cloneDeep } from "lodash";
1✔
3
import React, { FC, createContext, useContext, useEffect, useMemo, useState } from "react";
1✔
4
import { v4 } from "uuid";
1✔
5

6
import { QuestionnaireDataMigrator } from "@/classes/QuestionnaireDataMigrator";
1✔
7

8
import { InitialApplication, InitialQuestionnaire } from "../../config/InitialValues";
1✔
9
import {
1✔
10
  APPROVE_APP,
11
  GET_APP,
12
  LAST_APP,
13
  REJECT_APP,
14
  INQUIRE_APP,
15
  REOPEN_APP,
16
  SAVE_APP,
17
  SUBMIT_APP,
18
  ApproveAppResp,
19
  GetAppResp,
20
  LastAppResp,
21
  InquireAppResp,
22
  RejectAppResp,
23
  ReopenAppResp,
24
  SaveAppResp,
25
  SubmitAppResp,
26
  ApproveAppInput,
27
  SaveAppInput,
28
  LIST_INSTITUTIONS,
29
  ListInstitutionsInput,
30
  ListInstitutionsResp,
31
} from "../../graphql";
32
import { Logger } from "../../utils";
1✔
33
import { FormInput as ApproveFormInput } from "../Questionnaire/ApproveFormDialog";
34

35
export type SetDataReturnType =
36
  | { status: "success"; id: string }
37
  | { status: "failed"; errorMessage: string };
38

39
export type ContextState = {
40
  status: Status;
41
  data: Application;
42
  submitData?: () => Promise<string | boolean>;
43
  reopenForm?: () => Promise<string | boolean>;
44
  approveForm?: (data: ApproveFormInput, wholeProgram: boolean) => Promise<SetDataReturnType>;
45
  inquireForm?: (comment: string) => Promise<string | boolean>;
46
  rejectForm?: (comment: string) => Promise<string | boolean>;
47
  setData?: (
48
    questionnaire: QuestionnaireData,
49
    opts?: { skipSave?: boolean }
50
  ) => Promise<SetDataReturnType>;
51
  error?: string;
52
};
53

54
export enum Status {
1✔
55
  LOADING = "LOADING", // Loading initial data
1✔
56
  LOADED = "LOADED", // Successfully loaded data
1✔
57
  ERROR = "ERROR", // Error loading data
1✔
58
  SAVING = "SAVING", // Saving data to the API
1✔
59
  SUBMITTING = "SUBMITTING", // Submitting data to the API
1✔
60
}
61

62
const initialState: ContextState = { status: Status.LOADING, data: null };
1✔
63

64
/**
65
 * Form Context
66
 *
67
 * NOTE: Do NOT use this context directly. Use the useFormContext hook instead.
68
 *       this is exported for testing purposes only.
69
 *
70
 * @see ContextState – Form context state
71
 * @see useFormContext – Form context hook
72
 */
73
export const Context = createContext<ContextState>(null);
1✔
74
Context.displayName = "FormContext";
1✔
75

76
/**
77
 * Form Context Hook
78
 *
79
 * @see FormProvider – Must be wrapped in a FormProvider component
80
 * @see ContextState – Form context state returned by the hook
81
 * @returns {ContextState} - Form context
82
 */
83
export const useFormContext = (): ContextState => {
1✔
84
  const context = useContext<ContextState>(Context);
1,101✔
85

86
  if (!context) {
1,101✔
87
    throw new Error("FormContext cannot be used outside of the FormProvider component");
4✔
88
  }
4✔
89

90
  return context;
1,096✔
91
};
1,096✔
92

93
type ProviderProps = {
94
  id: string;
95
  children: React.ReactNode;
96
};
97

98
/**
99
 * Creates a form context for the given form ID
100
 *
101
 * @see useFormContext – Form context hook
102
 * @param {ProviderProps} props - Form context provider props
103
 * @returns {JSX.Element} - Form context provider
104
 */
105
export const FormProvider: FC<ProviderProps> = ({ children, id }: ProviderProps) => {
1✔
106
  const [state, setState] = useState<ContextState>(initialState);
92✔
107

108
  const [getInstitutions] = useLazyQuery<ListInstitutionsResp, ListInstitutionsInput>(
92✔
109
    LIST_INSTITUTIONS,
92✔
110
    {
92✔
111
      variables: { first: -1, orderBy: "name", sortDirection: "asc" },
92✔
112
      context: { clientName: "backend" },
92✔
113
      fetchPolicy: "cache-first",
92✔
114
      onError: (e) => Logger.error("FormContext listInstitutions API error:", e),
92✔
115
    }
92✔
116
  );
92✔
117

118
  const [lastApp] = useLazyQuery<LastAppResp>(LAST_APP, {
92✔
119
    context: { clientName: "backend" },
92✔
120
    fetchPolicy: "no-cache",
92✔
121
  });
92✔
122

123
  const [getApp] = useLazyQuery<GetAppResp>(GET_APP, {
92✔
124
    variables: { id },
92✔
125
    context: { clientName: "backend" },
92✔
126
    fetchPolicy: "no-cache",
92✔
127
  });
92✔
128

129
  const [saveApp] = useMutation<SaveAppResp, SaveAppInput>(SAVE_APP, {
92✔
130
    context: { clientName: "backend" },
92✔
131
    fetchPolicy: "no-cache",
92✔
132
  });
92✔
133

134
  const [submitApp] = useMutation<SubmitAppResp>(SUBMIT_APP, {
92✔
135
    variables: { id },
92✔
136
    context: { clientName: "backend" },
92✔
137
    fetchPolicy: "no-cache",
92✔
138
  });
92✔
139

140
  const [reopenApp] = useMutation<ReopenAppResp>(REOPEN_APP, {
92✔
141
    variables: { id },
92✔
142
    context: { clientName: "backend" },
92✔
143
    fetchPolicy: "no-cache",
92✔
144
  });
92✔
145

146
  const [approveApp] = useMutation<ApproveAppResp, ApproveAppInput>(APPROVE_APP, {
92✔
147
    context: { clientName: "backend" },
92✔
148
    fetchPolicy: "no-cache",
92✔
149
  });
92✔
150

151
  const [inquireApp] = useMutation<InquireAppResp>(INQUIRE_APP, {
92✔
152
    variables: { id },
92✔
153
    context: { clientName: "backend" },
92✔
154
    fetchPolicy: "no-cache",
92✔
155
  });
92✔
156

157
  const [rejectApp] = useMutation<RejectAppResp>(REJECT_APP, {
92✔
158
    variables: { id },
92✔
159
    context: { clientName: "backend" },
92✔
160
    fetchPolicy: "no-cache",
92✔
161
  });
92✔
162

163
  const setData = async (
92✔
NEW
164
    data: QuestionnaireData,
×
NEW
165
    opts?: { skipSave?: boolean }
×
NEW
166
  ): Promise<SetDataReturnType> => {
×
167
    const newState = {
×
168
      ...state,
×
169
      data: {
×
170
        ...state.data,
×
171
        questionnaireData: data,
×
172
      },
×
173
    };
×
174

175
    setState((prevState) => ({ ...prevState, status: Status.SAVING }));
×
176
    const fullPIName = `${data?.pi?.firstName || ""} ${data?.pi?.lastName || ""}`.trim();
×
177

178
    const newInstitutions = [...newState.data.newInstitutions];
×
179
    const { pi, primaryContact, additionalContacts } = newState.data.questionnaireData;
×
180
    const contacts = [pi, primaryContact, ...(additionalContacts || [])].filter(
×
181
      (obj) => obj && (obj.institution || obj.institutionID)
×
182
    );
×
183

184
    contacts.forEach((contact) => {
×
185
      if (contact.institutionID) {
×
186
        return;
×
187
      }
×
188

189
      const prevId = newInstitutions.find(({ name }) => name === contact.institution)?.id;
×
190
      if (prevId) {
×
191
        contact.institutionID = prevId;
×
192
      } else {
×
193
        const newId = v4();
×
194
        newInstitutions.push({ id: newId, name: contact.institution });
×
195
        contact.institutionID = newId;
×
196
      }
×
197
    });
×
198

NEW
199
    if (opts?.skipSave) {
×
NEW
200
      setState({ ...newState, status: Status.LOADED, error: null });
×
NEW
201
      return {
×
NEW
202
        status: "success",
×
NEW
203
        id: newState.data._id,
×
NEW
204
      };
×
NEW
205
    }
×
206

207
    const { data: d, errors } = await saveApp({
×
208
      variables: {
×
209
        application: {
×
210
          _id: newState?.data?._id === "new" ? undefined : newState?.data?._id,
×
211
          studyName: data?.study?.name,
×
212
          studyAbbreviation: data?.study?.abbreviation || data?.study?.name,
×
213
          questionnaireData: JSON.stringify(data),
×
214
          controlledAccess: data?.accessTypes?.includes("Controlled Access") || false,
×
215
          openAccess: data?.accessTypes?.includes("Open Access") || false,
×
216
          ORCID: data?.pi?.ORCID,
×
217
          PI: fullPIName,
×
218
          programName: data?.program?.name,
×
219
          programAbbreviation: data?.program?.abbreviation,
×
220
          programDescription: data?.program?.description,
×
221
          newInstitutions: newInstitutions
×
222
            .filter((inst) => contacts.findIndex((c) => c.institutionID === inst.id) !== -1)
×
223
            .map(({ id, name }) => ({ id, name })),
×
224
          GPAName: data?.study?.GPAName,
×
225
        },
×
226
      },
×
227
    }).catch((e) => ({ data: null, errors: [e] }));
×
228

229
    if (errors || !d?.saveApplication?._id) {
×
230
      const errorMessage = errors?.[0]?.message || "An unknown GraphQL Error occurred";
×
231

232
      Logger.error("Unable to save application", errors);
×
233
      setState({
×
234
        ...newState,
×
235
        status: Status.ERROR,
×
236
        error: errorMessage,
×
237
      });
×
238

239
      return {
×
240
        status: "failed",
×
241
        errorMessage,
×
242
      };
×
243
    }
×
244

245
    // eslint-disable-next-line @typescript-eslint/dot-notation
246
    if (d?.saveApplication?._id && data["_id"] === "new") {
×
247
      newState.data = {
×
248
        ...newState.data,
×
249
        _id: d.saveApplication._id,
×
250
        applicant: d?.saveApplication?.applicant,
×
251
      };
×
252
    }
×
253

254
    newState.data = {
×
255
      ...newState.data,
×
256
      status: d?.saveApplication?.status,
×
257
      updatedAt: d?.saveApplication?.updatedAt,
×
258
      createdAt: d?.saveApplication?.createdAt,
×
259
      submittedDate: d?.saveApplication?.submittedDate,
×
260
      history: d?.saveApplication?.history,
×
261
      newInstitutions: d?.saveApplication?.newInstitutions,
×
262
    };
×
263

264
    setState({ ...newState, status: Status.LOADED, error: null });
×
265
    return {
×
266
      status: "success",
×
267
      id: d.saveApplication._id,
×
268
    };
×
269
  };
×
270

271
  const submitData = async () => {
92✔
272
    setState((prevState) => ({ ...prevState, status: Status.SUBMITTING }));
×
273

274
    const { data: res, errors } = await submitApp({
×
275
      variables: {
×
276
        _id: state?.data._id,
×
277
      },
×
278
    });
×
279

280
    if (errors) {
×
281
      setState((prevState) => ({ ...prevState, status: Status.LOADED }));
×
282
      return false;
×
283
    }
×
284

285
    setState((prevState) => ({ ...prevState, status: Status.LOADED }));
×
286
    return res?.submitApplication?._id || false;
×
287
  };
×
288

289
  // Here we approve the form to the API with a comment and wholeProgram
290
  const approveForm = async (
92✔
291
    data: ApproveFormInput,
3✔
292
    wholeProgram: boolean
3✔
293
  ): Promise<SetDataReturnType> => {
3✔
294
    setState((prevState) => ({ ...prevState, status: Status.SUBMITTING }));
3✔
295

296
    const { data: res, errors } = await approveApp({
3✔
297
      variables: {
3✔
298
        id: state?.data?._id,
3✔
299
        comment: data?.reviewComment,
3✔
300
        wholeProgram,
3✔
301
        pendingModelChange: data?.pendingModelChange,
3✔
302
      },
3✔
303
    }).catch((e) => ({ data: null, errors: [e] }));
3✔
304

305
    if (errors || !res?.approveApplication?._id) {
3✔
306
      setState((prevState) => ({ ...prevState, status: Status.ERROR }));
2✔
307
      return {
2✔
308
        status: "failed",
2✔
309
        errorMessage: errors?.[0]?.message || "An unknown GraphQL Error occurred",
2!
310
      };
2✔
311
    }
2✔
312

313
    setState((prevState) => ({ ...prevState, status: Status.LOADED }));
1✔
314
    return {
1✔
315
      status: "success",
1✔
316
      id: res?.approveApplication?._id,
1✔
317
    };
3✔
318
  };
3✔
319

320
  // Here we set the form to inquired through the API with a comment
321
  const inquireForm = async (comment: string) => {
92✔
322
    setState((prevState) => ({ ...prevState, status: Status.SUBMITTING }));
3✔
323

324
    const { data: res, errors } = await inquireApp({
3✔
325
      variables: {
3✔
326
        _id: state?.data._id,
3✔
327
        comment,
3✔
328
      },
3✔
329
    }).catch((e) => ({ data: null, errors: [e] }));
3✔
330

331
    if (errors || !res?.inquireApplication?._id) {
3✔
332
      setState((prevState) => ({ ...prevState, status: Status.ERROR }));
2✔
333
      return false;
2✔
334
    }
2✔
335

336
    setState((prevState) => ({ ...prevState, status: Status.LOADED }));
1✔
337
    return res?.inquireApplication?._id;
1✔
338
  };
3✔
339

340
  // Here we reject the form to the API with a comment
341
  const rejectForm = async (comment: string) => {
92✔
342
    setState((prevState) => ({ ...prevState, status: Status.SUBMITTING }));
3✔
343

344
    const { data: res, errors } = await rejectApp({
3✔
345
      variables: {
3✔
346
        _id: state?.data._id,
3✔
347
        comment,
3✔
348
      },
3✔
349
    }).catch((e) => ({ data: null, errors: [e] }));
3✔
350

351
    if (errors || !res?.rejectApplication?._id) {
3✔
352
      setState((prevState) => ({ ...prevState, status: Status.ERROR }));
2✔
353
      return false;
2✔
354
    }
2✔
355

356
    setState((prevState) => ({ ...prevState, status: Status.LOADED }));
1✔
357
    return res?.rejectApplication?._id;
1✔
358
  };
3✔
359

360
  // Reopen a form when it has been rejected and they submit an updated form
361
  const reopenForm = async () => {
92✔
362
    setState((prevState) => ({ ...prevState, status: Status.LOADING }));
3✔
363

364
    const { data: res, errors } = await reopenApp({
3✔
365
      variables: {
3✔
366
        _id: state?.data._id,
3✔
367
      },
3✔
368
    }).catch((e) => ({ data: null, errors: [e] }));
3✔
369

370
    if (errors || !res?.reopenApplication?._id) {
3✔
371
      setState((prevState) => ({ ...prevState, status: Status.ERROR }));
2✔
372
      return false;
2✔
373
    }
2✔
374

375
    setState((prevState) => ({
1✔
376
      ...prevState,
1✔
377
      data: {
1✔
378
        ...prevState?.data,
1✔
379
        ...res?.reopenApplication,
1✔
380
      },
1✔
381
      status: Status.LOADED,
1✔
382
    }));
1✔
383
    return res?.reopenApplication?._id;
1✔
384
  };
3✔
385

386
  useEffect(() => {
92✔
387
    if (!id || !id.trim()) {
20✔
388
      setState({
1✔
389
        status: Status.ERROR,
1✔
390
        data: null,
1✔
391
        error: "Invalid application ID provided",
1✔
392
      });
1✔
393
      return;
1✔
394
    }
1✔
395

396
    (async () => {
19✔
397
      // NOTE: This logic is UNUSED but left as a fallback in case we need to revert to it
398
      if (id === "new") {
19✔
399
        const { data: d } = await lastApp();
2✔
400
        const { getMyLastApplication } = d || {};
2!
401
        const lastAppData = JSON.parse(getMyLastApplication?.questionnaireData || null) || {};
2✔
402

403
        setState({
2✔
404
          status: Status.LOADED,
2✔
405
          data: {
2✔
406
            ...InitialApplication,
2✔
407
            questionnaireData: {
2✔
408
              ...InitialQuestionnaire,
2✔
409
              pi: {
2✔
410
                ...InitialQuestionnaire.pi,
2✔
411
                ...lastAppData?.pi,
2✔
412
              },
2✔
413
            },
2✔
414
          },
2✔
415
        });
2✔
416

417
        return;
2✔
418
      }
2✔
419

420
      const { data: d, error } = await getApp();
17✔
421
      if (error || !d?.getApplication?.questionnaireData) {
19✔
422
        setState({
2✔
423
          status: Status.ERROR,
2✔
424
          data: null,
2✔
425
          error: "An unknown API or GraphQL error occurred",
2✔
426
        });
2✔
427
        return;
2✔
428
      }
2✔
429

430
      const { getApplication } = d;
15✔
431
      const questionnaireData: QuestionnaireData = JSON.parse(
15✔
432
        getApplication?.questionnaireData || null
19!
433
      );
19✔
434

435
      const migrator = new QuestionnaireDataMigrator(questionnaireData, {
19✔
436
        getInstitutions,
19✔
437
        newInstitutions: getApplication?.newInstitutions || [],
19✔
438
        getLastApplication: lastApp,
19✔
439
      });
19✔
440
      const migratedData = await migrator.run();
19✔
441

442
      setState({
15✔
443
        status: Status.LOADED,
15✔
444
        data: {
15✔
445
          ...merge(cloneDeep(InitialApplication), d?.getApplication),
19✔
446
          questionnaireData: {
19✔
447
            ...merge(cloneDeep(InitialQuestionnaire), migratedData),
19✔
448
          },
19✔
449
        },
19✔
450
      });
19✔
451
    })();
19✔
452
  }, [id]);
92✔
453

454
  const value = useMemo(
92✔
455
    () => ({
92✔
456
      ...state,
52✔
457
      setData,
52✔
458
      submitData,
52✔
459
      approveForm,
52✔
460
      inquireForm,
52✔
461
      rejectForm,
52✔
462
      reopenForm,
52✔
463
    }),
52✔
464
    [state]
92✔
465
  );
92✔
466

467
  return <Context.Provider value={value}>{children}</Context.Provider>;
92✔
468
};
92✔
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