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

CBIIT / crdc-datahub-ui / 17135186002

21 Aug 2025 06:09PM UTC coverage: 77.612% (+1.7%) from 75.941%
17135186002

Pull #806

github

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

4850 of 5333 branches covered (90.94%)

Branch coverage included in aggregate %.

3174 of 3450 new or added lines in 33 files covered. (92.0%)

7 existing lines in 3 files now uncovered.

29048 of 38343 relevant lines covered (75.76%)

175.51 hits per line

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

9.92
/src/content/questionnaire/sections/A.tsx
1
import { parseForm } from "@jalik/form-parser";
1✔
2
import AddCircleIcon from "@mui/icons-material/AddCircle";
1✔
3
import { Checkbox, FormControlLabel, Grid, styled } from "@mui/material";
1✔
4
import { cloneDeep, unset } from "lodash";
1✔
5
import { FC, useEffect, useRef, useState } from "react";
1✔
6
import { useLocation } from "react-router-dom";
1✔
7

8
import useAggregatedInstitutions from "@/hooks/useAggregatedInstitutions";
1✔
9
import useFormMode from "@/hooks/useFormMode";
1✔
10

11
import AddRemoveButton from "../../../components/AddRemoveButton";
1✔
12
import { Status as FormStatus, useFormContext } from "../../../components/Contexts/FormContext";
1✔
13
import PansBanner from "../../../components/PansBanner";
1✔
14
import AdditionalContact from "../../../components/Questionnaire/AdditionalContact";
1✔
15
import AutocompleteInput from "../../../components/Questionnaire/AutocompleteInput";
1✔
16
import FormContainer from "../../../components/Questionnaire/FormContainer";
1✔
17
import SectionGroup from "../../../components/Questionnaire/SectionGroup";
1✔
18
import TextInput from "../../../components/Questionnaire/TextInput";
1✔
19
import TransitionGroupWrapper from "../../../components/Questionnaire/TransitionGroupWrapper";
1✔
20
import { InitialQuestionnaire } from "../../../config/InitialValues";
1✔
21
import SectionMetadata from "../../../config/SectionMetadata";
1✔
22
import {
1✔
23
  combineQuestionnaireData,
24
  filterForNumbers,
25
  formatORCIDInput,
26
  isValidORCID,
27
  mapObjectWithKey,
28
  validateEmail,
29
  validateUTF8,
30
} from "../../../utils";
31

32
export type KeyedContact = {
33
  key: string;
34
} & Contact;
35

36
const StyledFormControlLabel = styled(FormControlLabel)({
1✔
37
  transform: "translateY(-15px)",
1✔
38
  "& .MuiFormControlLabel-label": {
1✔
39
    color: "#083A50",
1✔
40
    fontWeight: "700",
1✔
41
    userSelect: "none",
1✔
42
  },
1✔
43
  "& .MuiCheckbox-root:not(.Mui-disabled)": {
1✔
44
    color: "#005EA2 !important",
1✔
45
  },
1✔
46
});
1✔
47

48
const HiddenField = styled("input")({
1✔
49
  display: "none",
1✔
50
});
1✔
51

52
/**
53
 * Form Section A View
54
 *
55
 * @param {FormSectionProps} props
56
 * @returns {JSX.Element}
57
 */
58
const FormSectionA: FC<FormSectionProps> = ({ SectionOption, refs }: FormSectionProps) => {
1✔
59
  const {
×
60
    status,
×
61
    data: { questionnaireData: data },
×
62
  } = useFormContext();
×
63
  const { data: institutionList } = useAggregatedInstitutions();
×
64
  const location = useLocation();
×
65
  const { readOnlyInputs } = useFormMode();
×
66
  const { A: SectionAMetadata } = SectionMetadata;
×
67

68
  const [pi, setPi] = useState<PI>(data?.pi);
×
69
  const [primaryContact, setPrimaryContact] = useState<Contact>(data?.primaryContact);
×
70
  const [piAsPrimaryContact, setPiAsPrimaryContact] = useState<boolean>(
×
71
    data?.piAsPrimaryContact || false
×
72
  );
×
73
  const [additionalContacts, setAdditionalContacts] = useState<KeyedContact[]>(
×
74
    data.additionalContacts?.map(mapObjectWithKey) || []
×
75
  );
×
76

77
  const formContainerRef = useRef<HTMLDivElement>();
×
78
  const formRef = useRef<HTMLFormElement>();
×
79
  const { getFormObjectRef } = refs;
×
80

81
  const togglePrimaryPI = () => {
×
82
    setPiAsPrimaryContact(!piAsPrimaryContact);
×
83
    setPrimaryContact(cloneDeep(InitialQuestionnaire.primaryContact));
×
84
  };
×
85

86
  const getFormObject = (): FormObject | null => {
×
87
    if (!formRef.current) {
×
88
      return null;
×
89
    }
×
90

91
    const formObject = parseForm(formRef.current, { nullify: false });
×
NEW
92
    const combinedData = combineQuestionnaireData(data, formObject);
×
93

94
    if (!formObject.additionalContacts || formObject.additionalContacts.length === 0) {
×
95
      combinedData.additionalContacts = [];
×
96
    }
×
97
    if (formObject.piAsPrimaryContact) {
×
98
      combinedData.primaryContact = null;
×
99
    }
×
100

NEW
101
    combinedData.additionalContacts?.forEach((ac) => unset(ac, "key"));
×
102

103
    return { ref: formRef, data: combinedData };
×
104
  };
×
105

106
  const addContact = () => {
×
NEW
107
    setAdditionalContacts((prev) => [
×
NEW
108
      ...prev,
×
109
      {
×
110
        key: `${additionalContacts.length}_${new Date().getTime()}`,
×
111
        position: "",
×
112
        firstName: "",
×
113
        lastName: "",
×
114
        email: "",
×
115
        phone: "",
×
116
        institution: "",
×
117
        institutionID: "",
×
118
      },
×
119
    ]);
×
120
  };
×
121

122
  const removeContact = (key: string) => {
×
NEW
123
    setAdditionalContacts((prev) => prev.filter((c) => c.key !== key));
×
124
  };
×
125

126
  const handlePIInstitutionChange = (value: string) => {
×
127
    const apiData = institutionList.find((i) => i.name === value);
×
128
    setPi((prev) => ({
×
129
      ...prev,
×
130
      institution: apiData?.name || value,
×
131
      institutionID: apiData?._id || "",
×
132
    }));
×
133
  };
×
134

135
  const handlePCInstitutionChange = (value: string) => {
×
136
    const apiData = institutionList.find((i) => i.name === value);
×
137
    setPrimaryContact((prev) => ({
×
138
      ...prev,
×
139
      institution: apiData?.name || value,
×
140
      institutionID: apiData?._id || "",
×
141
    }));
×
142
  };
×
143

144
  useEffect(() => {
×
145
    getFormObjectRef.current = getFormObject;
×
146
  }, [refs]);
×
147

148
  useEffect(() => {
×
149
    if (location?.state?.from === "/submission-requests") {
×
150
      return;
×
151
    }
×
152

153
    formContainerRef.current?.scrollIntoView({ block: "start" });
×
154
  }, [location]);
×
155

NEW
156
  useEffect(() => {
×
NEW
157
    setPi(data?.pi);
×
NEW
158
  }, [data?.pi]);
×
159

NEW
160
  useEffect(() => {
×
NEW
161
    setPrimaryContact(data?.primaryContact);
×
NEW
162
  }, [data?.primaryContact]);
×
163

NEW
164
  useEffect(() => {
×
NEW
165
    setPiAsPrimaryContact(data?.piAsPrimaryContact || false);
×
NEW
166
  }, [data?.piAsPrimaryContact]);
×
167

NEW
168
  useEffect(() => {
×
NEW
169
    const incoming = data?.additionalContacts ?? [];
×
NEW
170
    setAdditionalContacts((prev) =>
×
NEW
171
      incoming.map((c, i) => ({
×
NEW
172
        ...c,
×
NEW
173
        key: prev[i]?.key ?? mapObjectWithKey(c, i).key,
×
NEW
174
      }))
×
NEW
175
    );
×
NEW
176
  }, [data?.additionalContacts]);
×
177

178
  return (
×
179
    <FormContainer
×
180
      ref={formContainerRef}
×
181
      formRef={formRef}
×
182
      description={SectionOption.title}
×
183
      prefixElement={<PansBanner />}
×
184
    >
185
      {/* Principal Investigator */}
186
      <SectionGroup
×
187
        title={SectionAMetadata.sections.PRINCIPAL_INVESTIGATOR.title}
×
188
        description={SectionAMetadata.sections.PRINCIPAL_INVESTIGATOR.description}
×
189
      >
190
        <TextInput
×
191
          id="section-a-pi-first-name"
×
192
          label="First name"
×
193
          name="pi[firstName]"
×
194
          value={pi?.firstName}
×
195
          placeholder="Enter first name"
×
196
          maxLength={50}
×
197
          required
×
198
          readOnly={readOnlyInputs}
×
199
        />
200
        <TextInput
×
201
          id="section-a-pi-last-name"
×
202
          label="Last name"
×
203
          name="pi[lastName]"
×
204
          value={pi?.lastName}
×
205
          placeholder="Enter last name"
×
206
          maxLength={50}
×
207
          required
×
208
          readOnly={readOnlyInputs}
×
209
        />
210
        <TextInput
×
211
          id="section-a-pi-position"
×
212
          label="Position"
×
213
          name="pi[position]"
×
214
          value={pi?.position}
×
215
          placeholder="Enter position"
×
216
          maxLength={100}
×
217
          required
×
218
          readOnly={readOnlyInputs}
×
219
        />
220
        <TextInput
×
221
          id="section-a-pi-email"
×
222
          type="email"
×
223
          label="Email"
×
224
          name="pi[email]"
×
225
          value={pi?.email}
×
226
          placeholder="Enter email"
×
227
          validate={validateEmail}
×
228
          errorText="Please provide a valid email address"
×
229
          required
×
230
          readOnly={readOnlyInputs}
×
231
        />
232
        <TextInput
×
233
          id="section-a-pi-orcid"
×
234
          label="ORCID"
×
235
          name="pi[ORCID]"
×
236
          value={pi?.ORCID}
×
237
          placeholder="e.g. 0000-0001-2345-6789"
×
238
          validate={(val) => val?.length === 0 || isValidORCID(val)}
×
239
          filter={formatORCIDInput}
×
240
          errorText="Please provide a valid ORCID"
×
241
          readOnly={readOnlyInputs}
×
242
        />
243
        <AutocompleteInput
×
244
          id="section-a-pi-institution"
×
245
          label="Institution"
×
246
          name="pi[institution]"
×
247
          value={pi?.institution || ""}
×
248
          options={institutionList?.map((i) => i.name)}
×
249
          placeholder="Enter or Select an Institution"
×
250
          validate={(v: string) => v?.trim()?.length > 0 && !validateUTF8(v)}
×
251
          onChange={(_, val) => handlePIInstitutionChange(val)}
×
252
          onInputChange={(_, val, reason) => {
×
253
            // NOTE: If reason is not 'input', then the user did not trigger this event
254
            if (reason === "input") {
×
255
              handlePIInstitutionChange(val);
×
256
            }
×
257
          }}
×
258
          required
×
259
          disableClearable
×
260
          freeSolo
×
261
          readOnly={readOnlyInputs}
×
262
        />
263
        <HiddenField
×
264
          type="text"
×
265
          name="pi[institutionID]"
×
266
          value={pi?.institutionID || ""}
×
267
          onChange={() => {}}
×
268
          data-type="string"
×
269
          aria-label="Institution ID field"
×
270
          hidden
×
271
        />
272
        <TextInput
×
273
          id="section-a-pi-institution-address"
×
274
          label="Institution Address"
×
275
          value={pi?.address}
×
276
          gridWidth={12}
×
277
          maxLength={200}
×
278
          name="pi[address]"
×
279
          placeholder="200 characters allowed"
×
280
          rows={4}
×
281
          multiline
×
282
          required
×
283
          readOnly={readOnlyInputs}
×
284
        />
285
      </SectionGroup>
×
286

287
      {/* Primary Contact */}
288
      <SectionGroup
×
289
        title={SectionAMetadata.sections.PRIMARY_CONTACT.title}
×
290
        description={SectionAMetadata.sections.PRIMARY_CONTACT.description}
×
291
      >
292
        <Grid item md={12}>
×
293
          <StyledFormControlLabel
×
294
            label="Same as Principal Investigator"
×
295
            control={
×
296
              <Checkbox
×
297
                checked={piAsPrimaryContact}
×
298
                onChange={() => !readOnlyInputs && togglePrimaryPI()}
×
299
                readOnly={readOnlyInputs}
×
300
              />
301
            }
302
            disabled={readOnlyInputs}
×
303
          />
304
          <input
×
305
            id="section-a-primary-contact-same-as-pi-checkbox"
×
306
            style={{ display: "none" }}
×
307
            type="checkbox"
×
308
            name="piAsPrimaryContact"
×
309
            data-type="boolean"
×
310
            value={piAsPrimaryContact?.toString()}
×
311
            aria-label="Same as Principal Investigator"
×
312
            checked
×
313
            readOnly
×
314
          />
315
        </Grid>
×
316
        {!piAsPrimaryContact && (
×
317
          <>
×
318
            <TextInput
×
319
              id="section-a-primary-contact-first-name"
×
320
              label="First name"
×
321
              name="primaryContact[firstName]"
×
322
              value={primaryContact?.firstName || ""}
×
323
              placeholder="Enter first name"
×
324
              maxLength={50}
×
325
              readOnly={readOnlyInputs}
×
326
              required
×
327
            />
328
            <TextInput
×
329
              id="section-a-primary-contact-last-name"
×
330
              label="Last name"
×
331
              name="primaryContact[lastName]"
×
332
              value={primaryContact?.lastName || ""}
×
333
              placeholder="Enter last name"
×
334
              maxLength={50}
×
335
              readOnly={readOnlyInputs}
×
336
              required
×
337
            />
338
            <TextInput
×
339
              id="section-a-primary-contact-position"
×
340
              label="Position"
×
341
              name="primaryContact[position]"
×
342
              value={primaryContact?.position || ""}
×
343
              placeholder="Enter position"
×
344
              maxLength={100}
×
345
              readOnly={readOnlyInputs}
×
346
              required
×
347
            />
348
            <TextInput
×
349
              id="section-a-primary-contact-email"
×
350
              type="email"
×
351
              label="Email"
×
352
              name="primaryContact[email]"
×
353
              value={primaryContact?.email || ""}
×
354
              validate={validateEmail}
×
355
              errorText="Please provide a valid email address"
×
356
              placeholder="Enter email"
×
357
              readOnly={readOnlyInputs}
×
358
              required
×
359
            />
360
            <AutocompleteInput
×
361
              id="section-a-primary-contact-institution"
×
362
              label="Institution"
×
363
              name="primaryContact[institution]"
×
364
              value={primaryContact?.institution || ""}
×
365
              options={institutionList?.map((i) => i.name)}
×
366
              placeholder="Enter or Select an Institution"
×
367
              readOnly={readOnlyInputs}
×
368
              validate={(v: string) => v?.trim()?.length > 0 && !validateUTF8(v)}
×
369
              onChange={(_, val) => handlePCInstitutionChange(val)}
×
370
              onInputChange={(_, val, reason) => {
×
371
                // NOTE: If reason is not 'input', then the user did not trigger this event
372
                if (reason === "input") {
×
373
                  handlePCInstitutionChange(val);
×
374
                }
×
375
              }}
×
376
              disableClearable
×
377
              required
×
378
              freeSolo
×
379
            />
380
            <HiddenField
×
381
              type="text"
×
382
              name="primaryContact[institutionID]"
×
383
              value={primaryContact?.institutionID || ""}
×
384
              onChange={() => {}}
×
385
              data-type="string"
×
386
              aria-label="Institution ID field"
×
387
              hidden
×
388
            />
389
            <TextInput
×
390
              id="section-a-primary-contact-phone-number"
×
391
              type="tel"
×
392
              label="Phone number"
×
393
              name="primaryContact[phone]"
×
394
              filter={filterForNumbers}
×
395
              value={primaryContact?.phone || ""}
×
396
              placeholder="Enter phone number"
×
397
              maxLength={25}
×
398
              readOnly={readOnlyInputs}
×
399
            />
400
          </>
×
401
        )}
402
      </SectionGroup>
×
403

404
      {/* Additional Contacts */}
405
      <SectionGroup
×
406
        title={SectionAMetadata.sections.ADDITIONAL_CONTACTS.title}
×
407
        description={SectionAMetadata.sections.ADDITIONAL_CONTACTS.description}
×
408
        endButton={
×
409
          <AddRemoveButton
×
410
            id="section-a-add-additional-contact-button"
×
411
            label="Add Contact"
×
412
            startIcon={<AddCircleIcon />}
×
413
            onClick={addContact}
×
414
            disabled={readOnlyInputs || status === FormStatus.SAVING}
×
415
          />
416
        }
417
      >
418
        <TransitionGroupWrapper
×
419
          items={additionalContacts}
×
420
          renderItem={(contact: KeyedContact, idx: number) => (
×
NEW
421
            <>
×
NEW
422
              <input
×
NEW
423
                type="hidden"
×
NEW
424
                name={`additionalContacts[${idx}][key]`}
×
NEW
425
                value={contact.key}
×
NEW
426
                readOnly
×
427
              />
NEW
428
              <AdditionalContact
×
NEW
429
                key={contact.key}
×
NEW
430
                idPrefix="section-a"
×
NEW
431
                index={idx}
×
NEW
432
                contact={contact}
×
NEW
433
                onDelete={() => removeContact(contact.key)}
×
NEW
434
                readOnly={readOnlyInputs}
×
435
              />
NEW
436
            </>
×
UNCOV
437
          )}
×
438
        />
439
      </SectionGroup>
×
440
    </FormContainer>
×
441
  );
442
};
×
443

444
export default FormSectionA;
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

© 2025 Coveralls, Inc