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

CSCfi / metadata-submitter-frontend / 15132822209

20 May 2025 08:35AM UTC coverage: 47.784% (-0.08%) from 47.859%
15132822209

push

github

Hang Le
Feature/fix formatting (merge commit)

Merge branch 'feature/fix-formatting' into 'main'
* Format all files missed by incorrect format script

* Update precommit for husky v9

* Fix formatting script to include all files

Closes #1033
See merge request https://gitlab.ci.csc.fi/sds-dev/sd-submit/metadata-submitter-frontend/-/merge_requests/1114

Approved-by: Hang Le <lhang@csc.fi>
Co-authored-by: Monika Radaviciute <mradavic@csc.fi>
Merged by Hang Le <lhang@csc.fi>

656 of 963 branches covered (68.12%)

Branch coverage included in aggregate %.

150 of 326 new or added lines in 48 files covered. (46.01%)

5 existing lines in 4 files now uncovered.

6353 of 13705 relevant lines covered (46.36%)

4.25 hits per line

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

62.75
/src/components/SubmissionWizard/WizardForms/WizardJSONSchemaParser.tsx
1
import * as React from "react"
1✔
2
import { useState, useEffect, useCallback } from "react"
1✔
3

4
import AddIcon from "@mui/icons-material/Add"
1✔
5
import ClearIcon from "@mui/icons-material/Clear"
1✔
6
import EventIcon from "@mui/icons-material/Event"
1✔
7
import HelpOutlineIcon from "@mui/icons-material/HelpOutline"
1✔
8
import LaunchIcon from "@mui/icons-material/Launch"
1✔
9
import RemoveIcon from "@mui/icons-material/Remove"
1✔
10
import DateAdapter from "@mui/lab/AdapterMoment"
1✔
11
import DatePicker from "@mui/lab/DatePicker"
1✔
12
import LocalizationProvider from "@mui/lab/LocalizationProvider"
1✔
13
import { FormControl, InputAdornment } from "@mui/material"
1✔
14
import Autocomplete from "@mui/material/Autocomplete"
1✔
15
import Box from "@mui/material/Box"
1✔
16
import Button from "@mui/material/Button"
1✔
17
import Checkbox from "@mui/material/Checkbox"
1✔
18
import Chip from "@mui/material/Chip"
1✔
19
import CircularProgress from "@mui/material/CircularProgress"
1✔
20
import FormControlLabel from "@mui/material/FormControlLabel"
1✔
21
import FormGroup from "@mui/material/FormGroup"
1✔
22
import FormHelperText from "@mui/material/FormHelperText"
1✔
23
import Grid from "@mui/material/Grid"
1✔
24
import IconButton from "@mui/material/IconButton"
1✔
25
import Paper from "@mui/material/Paper"
1✔
26
import { styled } from "@mui/material/styles"
1✔
27
import { TypographyVariant } from "@mui/material/styles/createTypography"
28
import TextField from "@mui/material/TextField"
1✔
29
import Tooltip, { TooltipProps } from "@mui/material/Tooltip"
1✔
30
import Typography from "@mui/material/Typography"
1✔
31
import { get, flatten, uniq, debounce } from "lodash"
1✔
32
import moment from "moment"
1✔
33
import { useFieldArray, useFormContext, useForm, Controller, useWatch } from "react-hook-form"
1✔
34
import { useTranslation } from "react-i18next"
1✔
35

36
import { DisplayObjectTypes, ObjectTypes } from "constants/wizardObject"
1✔
37
import { setAutocompleteField } from "features/autocompleteSlice"
1✔
38
import { useAppSelector, useAppDispatch } from "hooks"
1✔
39
import rorAPIService from "services/rorAPI"
1✔
40
import { ConnectFormChildren, ConnectFormMethods, FormObject, NestedField } from "types"
41
import { pathToName, traverseValues, getPathName } from "utils/JSONSchemaUtils"
1✔
42

43
/*
44
 * Highlight style for required fields
45
 */
46
const highlightStyle = theme => {
1✔
47
  return {
×
48
    borderColor: theme.palette.primary.main,
×
49
    borderWidth: 2,
×
50
  }
×
51
}
×
52

53
const BaselineDiv = styled("div")({
1✔
54
  display: "flex",
1✔
55
  alignItems: "baseline",
1✔
56
})
1✔
57

58
const FieldTooltip = styled(({ className, ...props }: TooltipProps) => (
1✔
59
  <Tooltip {...props} classes={{ popper: className }} />
44✔
60
))(({ theme }) => ({
1✔
61
  "& .MuiTooltip-tooltip": {
44✔
62
    padding: "2rem",
44✔
63
    backgroundColor: theme.palette.common.white,
44✔
64
    color: theme.palette.secondary.main,
44✔
65
    fontSize: "1.4rem",
44✔
66
    boxShadow: theme.shadows[1],
44✔
67
    border: `0.1rem solid ${theme.palette.primary.main}`,
44✔
68
    maxWidth: "25rem",
44✔
69
  },
44✔
70
  "& .MuiTooltip-arrow": {
44✔
71
    "&:before": {
44✔
72
      border: `0.1rem solid ${theme.palette.primary.main}`,
44✔
73
    },
44✔
74
    color: theme.palette.common.white,
44✔
75
  },
44✔
76
}))
1✔
77

78
const TooltipIcon = styled(HelpOutlineIcon)(({ theme }) => ({
1✔
79
  color: theme.palette.primary.main,
68✔
80
  marginLeft: "1rem",
68✔
81
}))
1✔
82

83
const DatePickerWrapper = styled(Grid)(({ theme }) => ({
1✔
84
  paddingLeft: 1,
×
85
  display: "flex",
×
86
  flexDirection: "row",
×
87
  alignItems: "center",
×
88
  "& > span:first-of-type": {
×
89
    fontSize: "0.875rem",
×
90
    color: theme.palette.primary.main,
×
91
    fontWeight: "bold",
×
92
    width: "3rem",
×
93
  },
×
94
  "& div": {
×
95
    padding: 0,
×
96
    margin: 0,
×
97
    height: "auto",
×
98
    "& input": {
×
99
      padding: 0,
×
100
      margin: 0,
×
101
    },
×
102
    "& > button": {
×
103
      color: theme.palette.primary.light,
×
104
    },
×
105
  },
×
106
}))
1✔
107

108
const DateCheckboxLabel = styled("span")(({ theme }) => ({
1✔
109
  fontSize: "0.8125rem",
×
110
  color: theme.palette.secondary.dark,
×
111
}))
1✔
112

113
/*
114
 * Clean up form values from empty strings and objects, translate numbers inside strings to numbers.
115
 */
116
const cleanUpFormValues = (data: unknown) => {
1✔
117
  const cleanedData = JSON.parse(JSON.stringify(data))
4✔
118
  return traverseFormValuesForCleanUp(cleanedData)
4✔
119
}
4✔
120

121
// Array is populated in traverseFields method.
122
const integerFields: Array<string> = []
1✔
123

124
const traverseFormValuesForCleanUp = (data: Record<string, unknown>) => {
1✔
125
  Object.keys(data).forEach(key => {
14✔
126
    const property = data[key] as Record<string, unknown> | string | null
63✔
127

128
    if (typeof property === "object" && !Array.isArray(property)) {
63✔
129
      if (property !== null) {
10✔
130
        data[key] = traverseFormValuesForCleanUp(property)
10✔
131
        if (Object.keys(property).length === 0) delete data[key]
10✔
132
      }
10✔
133
    }
10✔
134
    if (property === "") {
63✔
135
      delete data[key]
43✔
136
    }
43✔
137
    // Integer typed fields are considered as string like ID's which are numbers.
138
    // Therefore these fields need to be handled as string in the form and cast as number
139
    // for backend operations. Eg. "1234" is converted to 1234 so it passes backend validation
140
    else if (integerFields.indexOf(key) > -1) {
20✔
141
      data[key] = Number(data[key])
1✔
142
    }
1✔
143
  })
14✔
144
  return data
14✔
145
}
14✔
146

147
/*
148
 * Build react-hook-form fields based on given schema
149
 */
150
const buildFields = (schema: FormObject) => {
1✔
151
  try {
24✔
152
    return traverseFields(schema, [])
24✔
153
  } catch (error) {
24!
154
    console.error(error)
×
155
  }
×
156
}
24✔
157

158
/*
159
 * Allow children components inside ConnectForm to pull react-hook-form objects and methods from context
160
 */
161
const ConnectForm = ({ children }: ConnectFormChildren) => {
1✔
162
  const methods = useFormContext()
236✔
163
  return children({ ...(methods as ConnectFormMethods) })
236✔
164
}
236✔
165

166
/*
167
 * Get defaultValue for options in a form. Used when rendering a saved/submitted form
168
 */
169
const getDefaultValue = (name: string, nestedField?: Record<string, unknown>) => {
1✔
170
  if (nestedField) {
68✔
171
    let result
1✔
172
    const path = name.split(".")
1✔
173
    // E.g. Case of DOI form - Formats's fields
174
    if (path[0] === "formats") {
1!
175
      const k = path[0]
×
176
      if (k in nestedField) {
×
177
        result = nestedField[k]
×
178
      } else {
×
179
        return
×
180
      }
×
181
    } else {
1✔
182
      for (let i = 1, n = path.length; i < n; ++i) {
1✔
183
        const k = path[i]
1✔
184

185
        if (nestedField && k in nestedField) {
1!
186
          result = nestedField[k]
×
187
        } else {
1✔
188
          return
1✔
189
        }
1✔
190
      }
1!
191
    }
×
192
    return result
×
193
  } else {
68✔
194
    return ""
67✔
195
  }
67✔
196
}
68✔
197

198
/*
199
 * Traverse fields recursively, return correct fields for given object or log error, if object type is not supported.
200
 */
201
const traverseFields = (
1✔
202
  object: FormObject,
156✔
203
  path: string[],
156✔
204
  requiredProperties?: string[],
156✔
205
  requireFirst?: boolean,
156✔
206
  nestedField?: NestedField
156✔
207
) => {
156✔
208
  const name = pathToName(path)
156✔
209
  const [lastPathItem] = path.slice(-1)
156✔
210
  const label = object.title ?? lastPathItem
156!
211
  const required = !!requiredProperties?.includes(lastPathItem) || requireFirst || false
156✔
212
  const description = object.description
156✔
213
  const autoCompleteIdentifiers = ["organisation", "name of the place of affiliation"]
156✔
214

215
  if (object.oneOf)
156✔
216
    return (
156✔
217
      <FormSection key={name} name={name} label={label} level={path.length}>
6✔
218
        <FormOneOfField key={name} path={path} object={object} required={required} />
6✔
219
      </FormSection>
6✔
220
    )
221

222
  switch (object.type) {
150✔
223
    case "object": {
156✔
224
      const properties =
66✔
225
        label === DisplayObjectTypes.dataset && path.length === 0
66!
226
          ? { title: object.properties["title"], description: object.properties["description"] }
×
227
          : object.properties
66✔
228

229
      return (
66✔
230
        <FormSection
66✔
231
          key={name}
66✔
232
          name={name}
66✔
233
          label={label}
66✔
234
          level={path.length}
66✔
235
          description={description}
66✔
236
          isTitleShown
66✔
237
        >
238
          {Object.keys(properties).map(propertyKey => {
66✔
239
            const property = properties[propertyKey] as FormObject
130✔
240
            const required = object?.else?.required ?? object.required
130!
241
            let requireFirstItem = false
130✔
242

243
            if (
130✔
244
              path.length === 0 &&
130✔
245
              propertyKey === "title" &&
47!
246
              !object.title.includes("DAC - Data Access Committee")
×
247
            ) {
130!
248
              requireFirstItem = true
×
249
            }
×
250
            // Require first field of section if parent section is a required property
251
            if (
130✔
252
              requireFirst ||
130✔
253
              requiredProperties?.includes(name) ||
129✔
254
              requiredProperties?.includes(Object.keys(properties)[0])
107✔
255
            ) {
130✔
256
              const parentProperty = Object.values(properties)[0] as { title: string }
23✔
257
              requireFirstItem = parentProperty.title === property.title ? true : false
23✔
258
            }
23✔
259

260
            return traverseFields(
130✔
261
              property,
130✔
262
              [...path, propertyKey],
130✔
263
              required,
130✔
264
              requireFirstItem,
130✔
265
              nestedField
130✔
266
            )
130✔
267
          })}
66✔
268
        </FormSection>
66✔
269
      )
270
    }
66✔
271
    case "string": {
156✔
272
      return object["enum"] ? (
52✔
273
        <FormSection key={name} name={name} label={label} level={path.length}>
6✔
274
          <FormSelectField
6✔
275
            key={name}
6✔
276
            name={name}
6✔
277
            label={label}
6✔
278
            options={object.enum}
6✔
279
            required={required}
6✔
280
            description={description}
6✔
281
          />
282
        </FormSection>
6✔
283
      ) : object.title === "Date" ? (
46!
284
        <FormDatePicker
×
285
          key={name}
×
286
          name={name}
×
287
          label={label}
×
288
          required={required}
×
289
          description={description}
×
290
        />
291
      ) : autoCompleteIdentifiers.some(value => label.toLowerCase().includes(value)) ? (
46✔
292
        <FormAutocompleteField
5✔
293
          key={name}
5✔
294
          name={name}
5✔
295
          label={label}
5✔
296
          required={required}
5✔
297
          description={description}
5✔
298
        />
1✔
299
      ) : name.includes("keywords") ? (
41!
300
        <FormSection key={name} name={name} label={label} level={path.length}>
×
301
          <FormTagField
×
302
            key={name}
×
303
            name={name}
×
304
            label={label}
×
305
            required={required}
×
306
            description={description}
×
307
          />
308
        </FormSection>
×
309
      ) : (
310
        <FormSection key={name} name={name} label={label} level={path.length}>
41✔
311
          <FormTextField
41✔
312
            key={name}
41✔
313
            name={name}
41✔
314
            label={label}
41✔
315
            required={required}
41✔
316
            description={description}
41✔
317
            nestedField={nestedField}
41✔
318
          />
319
        </FormSection>
41✔
320
      )
321
    }
52✔
322
    case "integer": {
156✔
323
      // List fields with integer type in schema. List is used as helper when cleaning up fields for backend.
324
      const fieldName = name.split(".").pop()
8✔
325
      if (fieldName && integerFields.indexOf(fieldName) < 0) integerFields.push(fieldName)
8✔
326

327
      return (
8✔
328
        <FormSection key={name} name={name} label={label} level={path.length}>
8✔
329
          <FormTextField
8✔
330
            key={name}
8✔
331
            name={name}
8✔
332
            label={label}
8✔
333
            required={required}
8✔
334
            description={description}
8✔
335
          />
336
        </FormSection>
8✔
337
      )
338
    }
8✔
339
    case "number": {
156✔
340
      return (
6✔
341
        <FormTextField
6✔
342
          key={name}
6✔
343
          name={name}
6✔
344
          label={label}
6✔
345
          required={required}
6✔
346
          description={description}
6✔
347
          type="number"
6✔
348
        />
349
      )
350
    }
6✔
351
    case "boolean": {
156✔
352
      return (
6✔
353
        <FormBooleanField
6✔
354
          key={name}
6✔
355
          name={name}
6✔
356
          label={label}
6✔
357
          required={required}
6✔
358
          description={description}
6✔
359
        />
360
      )
361
    }
6✔
362
    case "array": {
156✔
363
      return object.items.enum ? (
12✔
364
        <FormSection key={name} name={name} label={label} level={path.length}>
6✔
365
          <FormCheckBoxArray
6✔
366
            key={name}
6✔
367
            name={name}
6✔
368
            label=""
6✔
369
            options={object.items.enum}
6✔
370
            required={required}
6✔
371
            description={description}
6✔
372
          />
373
        </FormSection>
6✔
374
      ) : (
375
        <FormArray
6✔
376
          key={name}
6✔
377
          object={object}
6✔
378
          path={path}
6✔
379
          required={required}
6✔
380
          description={description}
6✔
381
        />
382
      )
383
    }
12✔
384
    case "null": {
156!
385
      return null
×
386
    }
×
387
    default: {
156!
388
      console.error(`
×
389
      No field parsing support for type ${object.type} yet.
×
390

391
      Pretty printed version of object with unsupported type:
392
      ${JSON.stringify(object, null, 2)}
×
393
      `)
×
394
      return null
×
395
    }
×
396
  }
156✔
397
}
156✔
398

399
const DisplayDescription = ({
1✔
400
  description,
5✔
401
  children,
5✔
402
}: {
403
  description: string
404
  children?: React.ReactElement
405
}) => {
5✔
406
  const { t } = useTranslation()
5✔
407
  const [isReadMore, setIsReadMore] = useState(description.length > 60)
5✔
408

409
  const toggleReadMore = () => {
5✔
410
    setIsReadMore(!isReadMore)
2✔
411
  }
2✔
412

413
  const ReadmoreText = styled("span")(({ theme }) => ({
5✔
414
    fontWeight: 700,
3✔
415
    textDecoration: "underline",
3✔
416
    display: "block",
3✔
417
    marginTop: "0.5rem",
3✔
418
    color: theme.palette.primary.main,
3✔
419
    "&:hover": { cursor: "pointer" },
3✔
420
  }))
5✔
421

422
  return (
5✔
423
    <p>
5✔
424
      {isReadMore ? `${description.slice(0, 60)}...` : description}
5✔
425
      {!isReadMore && children}
5✔
426
      {description?.length >= 60 && (
5✔
427
        <ReadmoreText onClick={toggleReadMore}>
4✔
428
          {isReadMore ? t("showMore") : t("showLess")}
4✔
429
        </ReadmoreText>
4✔
430
      )}
431
    </p>
5✔
432
  )
433
}
5✔
434

435
type FormSectionProps = {
436
  name: string
437
  label: string
438
  level: number
439
  isTitleShown?: boolean
440
  children?: React.ReactNode
441
}
442

443
const FormSectionTitle = styled(Paper, { shouldForwardProp: prop => prop !== "level" })<{
1✔
444
  level: number
445
}>(({ theme, level }) => ({
1✔
446
  display: "flex",
66✔
447
  flexDirection: "column",
66✔
448
  justifyContent: "start",
66✔
449
  alignItems: "start",
66✔
450
  backgroundColor: level === 1 ? theme.palette.primary.lighter : theme.palette.common.white,
66✔
451
  height: "100%",
66✔
452
  marginLeft: level <= 1 ? "5rem" : 0,
66!
453
  marginRight: "3rem",
66✔
454
  padding: level === 0 ? "4rem 0 3rem 0" : level === 1 ? "2rem" : 0,
66!
455
}))
1✔
456

457
/*
458
 * FormSection is rendered for properties with type object
459
 */
460
const FormSection = ({
1✔
461
  name,
133✔
462
  label,
133✔
463
  level,
133✔
464
  children,
133✔
465
  description,
133✔
466
  isTitleShown,
133✔
467
}: FormSectionProps & { description?: string }) => {
133✔
468
  const splittedPath = name.split(".") // Have a fully splitted path for names such as "studyLinks.0", "dacLinks.0"
133✔
469

470
  const heading = (
133✔
471
    <Grid size={{ xs: 12, md: level === 0 ? 12 : level === 1 ? 4 : 8 }}>
133✔
472
      {(level <= 1 || ((level === 3 || level === 2) && isTitleShown)) && label && (
133✔
473
        <FormSectionTitle square={true} elevation={0} level={level}>
66✔
474
          <Typography
66✔
475
            key={`${name}-header`}
66✔
476
            variant={level === 0 ? "h4" : ("subtitle1" as TypographyVariant)}
66✔
477
            role="heading"
66✔
478
            color="secondary"
66✔
479
          >
480
            {label} {name.includes("keywords") ? "*" : ""}
66!
481
            {description && level === 1 && (
66✔
482
              <FieldTooltip
11✔
483
                title={<DisplayDescription description={description} />}
11✔
484
                placement="top"
11✔
485
                arrow
11✔
486
                describeChild
11✔
487
              >
488
                <TooltipIcon />
11✔
489
              </FieldTooltip>
11✔
490
            )}
491
          </Typography>
66✔
492
        </FormSectionTitle>
66✔
493
      )}
494
    </Grid>
133✔
495
  )
496

497
  return (
133✔
498
    <ConnectForm>
133✔
499
      {({ errors }: ConnectFormMethods) => {
133✔
500
        const error = get(errors, name)
133✔
501
        return (
133✔
502
          <>
133✔
503
            <Grid
133✔
504
              container
133✔
505
              key={`${name}-section`}
133✔
506
              sx={{ mb: level <= 1 && splittedPath.length <= 1 ? "3rem" : 0 }}
133✔
507
            >
508
              {heading}
133✔
509
              <Grid size={{ xs: 12, md: level === 1 && label ? 8 : 12 }}>{children}</Grid>
133✔
510
            </Grid>
133✔
511
            <div>
133✔
512
              {error ? (
133!
513
                <FormControl error>
×
514
                  <FormHelperText>
×
515
                    {label} {error?.message}
×
516
                  </FormHelperText>
×
517
                </FormControl>
×
518
              ) : null}
133✔
519
            </div>
133✔
520
          </>
133✔
521
        )
522
      }}
133✔
523
    </ConnectForm>
133✔
524
  )
525
}
133✔
526

527
/*
528
 * FormOneOfField is rendered if property can be choosed from many possible.
529
 */
530

531
const FormOneOfField = ({
1✔
532
  path,
9✔
533
  object,
9✔
534
  nestedField,
9✔
535
  required,
9✔
536
}: {
537
  path: string[]
538
  object: FormObject
539
  nestedField?: NestedField
540
  required?: boolean
541
}) => {
9✔
542
  const options = object.oneOf
9✔
543
  const [lastPathItem] = path.slice(-1)
9✔
544
  const description = object.description
9✔
545
  // Get the fieldValue when rendering a saved/submitted form
546
  // For e.g. obj.required is ["label", "url"] and nestedField is {id: "sth1", label: "sth2", url: "sth3"}
547
  // Get object from state and set default values if child of oneOf field has values
548
  // Fetching current object values from state rather than via getValues() method gets values on first call. The getValues method results in empty object on first call
549
  const currentObject = useAppSelector(state => state.currentObject) || {}
9!
550
  const values = currentObject[path.toString()]
9!
551
    ? currentObject
×
552
    : currentObject[
9✔
553
        Object.keys(currentObject)
9✔
554
          .filter(item => path.includes(item))
9✔
555
          .toString()
9✔
556
      ] || {}
9✔
557

558
  let fieldValue: string | number | undefined
9✔
559

560
  const flattenObject = (obj: { [x: string]: never }, prefix = "") =>
9✔
561
    Object.keys(obj).reduce(
×
562
      (acc, k) => {
×
563
        const pre = prefix.length ? prefix + "." : ""
×
564
        if (typeof obj[k] === "object") Object.assign(acc, flattenObject(obj[k], pre + k))
×
565
        else acc[pre + k] = obj[k]
×
566
        return acc
×
567
      },
×
NEW
568
      {} as Record<string, string>
×
569
    )
×
570

571
  if (Object.keys(values).length > 0 && lastPathItem !== "prevStepIndex") {
9!
572
    for (const item of path) {
×
573
      if (values[item]) {
×
574
        const itemValues = values[item]
×
575
        const parentPath = Object.keys(itemValues) ? Object.keys(itemValues).toString() : ""
×
576
        // Match key from currentObject to option property.
577
        // Field key can be deeply nested and therefore we need to have multiple cases for finding correct value.
578
        if (isNaN(Number(parentPath[0]))) {
×
579
          fieldValue = (
×
580
            options.find(option => option.properties[parentPath])
×
581
              ? // Eg. Sample > Sample Names > Sample Data Type
582
                options.find(option => option.properties[parentPath])
×
583
              : // Eg. Run > Run Type > Reference Alignment
584
                options.find(
×
585
                  option =>
×
586
                    option.properties[
×
587
                      Object.keys(flattenObject(itemValues))[0].split(".").slice(-1)[0]
×
588
                    ]
589
                )
×
590
          )?.title as string
×
591
        } else {
×
592
          // Eg. Experiment > Expected Base Call Table > Processing > Single Processing
593
          if (typeof itemValues === "string") {
×
594
            fieldValue = options.find(option => option.type === "string")?.title
×
595
          }
×
596
          // Eg. Experiment > Expected Base Call Table > Processing > Complex Processing
597
          else {
×
598
            const fieldKey = Object.keys(values[item][0])[0]
×
599
            fieldValue = options?.find(option => option.items?.properties[fieldKey])?.title
×
600
          }
×
601
        }
×
602
      }
×
603
    }
×
604
  }
×
605

606
  // Eg. Study > Study Links
607
  if (nestedField) {
9✔
608
    for (const option of options) {
2✔
609
      option.required.every(
4✔
610
        (val: string) =>
4✔
611
          nestedField.fieldValues && Object.keys(nestedField.fieldValues).includes(val)
4!
612
      )
4!
613
        ? (fieldValue = option.title)
×
614
        : ""
4✔
615
    }
4✔
616
  }
2✔
617

618
  // Special handling for Expected Base Call Table > Processing > Complex Processing > Pipeline > Pipe Section > Prev Step Index
619
  // Can be extended to other fields if needed
620
  const itemValue = get(currentObject, pathToName(path))
9✔
621

622
  if (itemValue) {
9!
623
    switch (lastPathItem) {
×
624
      case "prevStepIndex":
×
625
        {
×
626
          fieldValue = "String value"
×
627
        }
×
628
        break
×
629
    }
×
630
  }
×
631

632
  const name = pathToName(path)
9✔
633

634
  const label = object.title ?? lastPathItem
9!
635

636
  type ChildObject = { properties: Record<string, unknown>; required: boolean }
637

638
  const getChildObjects = (obj?: ChildObject) => {
9✔
639
    if (obj) {
×
640
      let childProps
×
641
      for (const key in obj) {
×
642
        // Check if object has nested "properties"
643
        if (key === "properties") {
×
644
          childProps = obj.properties
×
645
          const childPropsValues = Object.values(childProps)[0]
×
646
          if (Object.hasOwnProperty.call(childPropsValues, "properties")) {
×
647
            getChildObjects(childPropsValues as ChildObject)
×
648
          }
×
649
        }
×
650
      }
×
651

652
      const firstProp = childProps ? Object.keys(childProps)[0] : ""
×
653
      return { obj, firstProp }
×
654
    }
×
655
    return {}
×
656
  }
×
657

658
  const [field, setField] = useState(fieldValue)
9✔
659
  const clearForm = useAppSelector(state => state.clearForm)
9✔
660

661
  return (
9✔
662
    <ConnectForm>
9✔
663
      {({ errors, unregister, setValue, getValues, reset }: ConnectFormMethods) => {
9✔
664
        if (clearForm) {
9!
665
          // Clear the field and "clearForm" is true
666
          setField("")
×
667
          unregister(name)
×
668
        }
×
669

670
        const error = get(errors, name)
9✔
671
        // Option change handling
672
        const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
9✔
673
          const val = event.target.value
2✔
674
          setField(val)
2✔
675

676
          // Get fieldValues of current path
677
          const currentFieldValues = getValues(name)
2✔
678
          // Unregister if selecting "Complex Processing", "Null value" in Experiment form
679
          if (val === "Complex Processing") unregister(name)
2!
680
          if (val === "Null value") setValue(name, null)
2!
681
          // Remove previous values of the same path
682
          if (
2✔
683
            val !== "Complex Processing" &&
2✔
684
            val !== "Null value" &&
2✔
685
            currentFieldValues !== undefined
2✔
686
          ) {
2!
687
            reset({ ...getValues(), [name]: "" })
×
688
          }
×
689
        }
2✔
690

691
        // Selected option
692
        const selectedOption =
9✔
693
          options?.filter((option: { title: string }) => option.title === field)[0]?.properties ||
9✔
694
          {}
7✔
695
        const selectedOptionValues = Object.values(selectedOption)
9✔
696

697
        let childObject
9✔
698
        let requiredProp: string
9✔
699

700
        // If selectedOption has many nested "properties"
701
        if (
9✔
702
          selectedOptionValues.length > 0 &&
9✔
703
          Object.hasOwnProperty.call(selectedOptionValues[0], "properties")
2✔
704
        ) {
9!
705
          const { obj, firstProp } = getChildObjects(
×
NEW
706
            Object.values(selectedOption)[0] as ChildObject
×
707
          )
×
708
          childObject = obj
×
709
          requiredProp = firstProp || ""
×
710
        }
×
711
        // Else if selectedOption has no nested "properties"
712
        else {
9✔
713
          childObject = options?.filter((option: { title: string }) => option.title === field)[0]
9✔
714
          requiredProp = childObject?.required?.toString() || Object.keys(selectedOption)[0]
9✔
715
        }
9✔
716

717
        let child
9✔
718
        if (field) {
9✔
719
          const fieldObject = options?.filter(
2✔
720
            (option: { title: string }) => option.title === field
2✔
721
          )[0]
2✔
722
          child = traverseFields(
2✔
723
            { ...fieldObject, title: "" },
2✔
724
            path,
2✔
725
            required && requiredProp ? requiredProp.split(",") : [],
2!
726
            childObject?.required ? false : true,
2✔
727
            nestedField
2✔
728
          )
2✔
729
        } else child = null
9✔
730

731
        return (
9✔
732
          <>
9✔
733
            <BaselineDiv>
9✔
734
              <TextField
9✔
735
                name={name}
9✔
736
                label={label}
9✔
737
                id={name}
9✔
738
                role="listbox"
9✔
739
                value={field || ""}
9✔
740
                select
9✔
741
                SelectProps={{ native: true }}
9✔
742
                onChange={event => {
9✔
743
                  handleChange(event)
2✔
744
                }}
2✔
745
                error={!!error}
9✔
746
                helperText={error?.message}
9!
747
                required={required}
9✔
748
                inputProps={{ "data-testid": name }}
9✔
749
                sx={{ mb: "1rem" }}
9✔
750
              >
751
                <option aria-label="None" value="" disabled />
9✔
752
                {options?.map((optionObject: { title: string }) => {
9✔
753
                  const option = optionObject.title
18✔
754
                  return (
18✔
755
                    <option key={`${name}-${option}`} value={option}>
18✔
756
                      {option}
18✔
757
                    </option>
18✔
758
                  )
759
                })}
9✔
760
              </TextField>
9✔
761
              {description && (
9!
762
                <FieldTooltip
×
763
                  title={<DisplayDescription description={description} />}
×
764
                  placement="right"
×
765
                  arrow
×
766
                  describeChild
×
767
                >
768
                  <TooltipIcon />
×
769
                </FieldTooltip>
×
770
              )}
771
            </BaselineDiv>
9✔
772
            {child}
9✔
773
          </>
9✔
774
        )
775
      }}
9✔
776
    </ConnectForm>
9✔
777
  )
778
}
9✔
779

780
type FormFieldBaseProps = {
781
  name: string
782
  label: string
783
  required: boolean
784
}
785

786
type FormSelectFieldProps = FormFieldBaseProps & { options: string[] }
787

788
/*
789
 * FormTextField is the most usual type, rendered for strings, integers and numbers.
790
 */
791
const FormTextField = ({
1✔
792
  name,
68✔
793
  label,
68✔
794
  required,
68✔
795
  description,
68✔
796
  type = "string",
68✔
797
  nestedField,
68✔
798
}: FormFieldBaseProps & { description: string; type?: string; nestedField?: NestedField }) => {
68✔
799
  const objectType = useAppSelector(state => state.objectType)
68✔
800
  const isDOIForm = objectType === ObjectTypes.datacite
68✔
801
  const autocompleteField = useAppSelector(state => state.autocompleteField)
68✔
802
  const path = name.split(".")
68✔
803
  const [lastPathItem] = path.slice(-1)
68✔
804

805
  // Default Value of input
806
  const defaultValue = getDefaultValue(name, nestedField)
68✔
807

808
  // Case: DOI form - Affilation fields to be prefilled
809
  const prefilledFields = ["affiliationIdentifier", "schemeUri", "affiliationIdentifierScheme"]
68✔
810
  let watchAutocompleteFieldName = ""
68✔
811
  let prefilledValue: null | undefined = null
68✔
812

813
  // Case: DOI form - Check if it's <creators>'s and <contributors>'s FullName field in DOI form
814
  const isFullNameField =
68✔
815
    (path[0] === "creators" || path[0] === "contributors") && path[2] === "name"
68!
816
  let fullNameValue = "" // Case: DOI form - Creators and Contributors' FullName
68✔
817

818
  let disabled = false // boolean if inputValue is disabled
68✔
819

820
  // useWatch to watch any changes in form's fields
821
  const watchValues = useWatch()
68✔
822

823
  if (isDOIForm) {
68!
824
    watchAutocompleteFieldName =
×
825
      name.includes("affiliation") && prefilledFields.includes(lastPathItem)
×
826
        ? getPathName(path, "name")
×
827
        : ""
×
828

829
    // check changes of value of autocompleteField from watchValues
830
    prefilledValue = watchAutocompleteFieldName
×
831
      ? get(watchValues, watchAutocompleteFieldName)
×
832
      : null
×
833

834
    // If it's <creators>'s and <contributors>'s FullName field, watch the values of GivenName and FamilyName
835
    if (isFullNameField) {
×
836
      const givenName = getPathName(path, "givenName")
×
837
      const givenNameValue = get(watchValues, givenName) || ""
×
838
      const familyName = getPathName(path, "familyName")
×
839
      const familyNameValue =
×
840
        get(watchValues, familyName)?.length > 0 ? get(watchValues, familyName).concat(",") : ""
×
841
      // Return value for FullName field
842
      fullNameValue = `${familyNameValue}${givenNameValue}`
×
843
    }
×
844

845
    // Conditions to disable input field: disable editing option if the field is rendered as prefilled
846
    disabled =
×
847
      (prefilledFields.includes(lastPathItem) && prefilledValue !== null) ||
×
848
      isFullNameField ||
×
849
      (defaultValue !== "" && name.includes("formats"))
×
850
  }
×
851

852
  /*
853
   * Handle DOI form values
854
   */
855
  const { setValue, getValues } = useFormContext()
68✔
856

857
  // Check value of current name path
858
  const val = getValues(name)
68✔
859

860
  // Set values for Affiliations' fields if autocompleteField exists
861
  useEffect(() => {
68✔
862
    if (prefilledValue && !val && isDOIForm) {
27!
863
      lastPathItem === prefilledFields[0] ? setValue(name, autocompleteField) : null
×
864
      lastPathItem === prefilledFields[1] ? setValue(name, "https://ror.org") : null
×
865
      lastPathItem === prefilledFields[2] ? setValue(name, "ROR") : null
×
866
    }
×
867
  }, [autocompleteField, prefilledValue])
68✔
868

869
  // Remove values for Affiliations' <location of affiliation identifier> field if autocompleteField is deleted
870
  useEffect(() => {
68✔
871
    if (prefilledValue === undefined && val && lastPathItem === prefilledFields[0] && isDOIForm)
27!
872
      setValue(name, "")
27!
873
  }, [prefilledValue])
68✔
874

875
  useEffect(() => {
68✔
876
    // Set value of <creators>'s and <contributors>'s FullName field with the fullNameValue from givenName and familyName
877
    if (isFullNameField && fullNameValue) setValue(name, fullNameValue)
27!
878
  }, [isFullNameField, fullNameValue])
68✔
879

880
  return (
68✔
881
    <ConnectForm>
68✔
882
      {({ control }: ConnectFormMethods) => {
68✔
883
        const multiLineRowIdentifiers = ["abstract", "description", "policy text"]
68✔
884

885
        return (
68✔
886
          <Controller
68✔
887
            render={({ field, fieldState: { error } }) => {
68✔
888
              const inputValue =
69✔
889
                (watchAutocompleteFieldName && typeof val !== "object" && val) ||
69!
890
                fullNameValue ||
69✔
891
                (typeof field.value !== "object" && field.value) ||
69✔
892
                ""
57✔
893

894
              const handleChange = (e: { target: { value: string | number } }) => {
69✔
895
                const { value } = e.target
11✔
896
                const parsedValue =
11✔
897
                  type === "string" && typeof value === "number" ? value.toString() : value
11!
898
                field.onChange(parsedValue) // Helps with Cypress change detection
11✔
899
                setValue(name, parsedValue) // Enables update of nested fields, eg. DAC contact
11✔
900
              }
11✔
901

902
              return (
69✔
903
                <BaselineDiv>
69✔
904
                  <TextField
69✔
905
                    {...field}
69✔
906
                    inputProps={{ "data-testid": name }}
69✔
907
                    label={label}
69✔
908
                    id={name}
69✔
909
                    role="textbox"
69✔
910
                    error={!!error}
69✔
911
                    helperText={error?.message}
69✔
912
                    required={required}
69✔
913
                    type={type}
69✔
914
                    multiline={multiLineRowIdentifiers.some(value =>
69✔
915
                      label.toLowerCase().includes(value)
196✔
916
                    )}
69✔
917
                    rows={5}
69✔
918
                    value={inputValue}
69✔
919
                    onChange={handleChange}
69✔
920
                    disabled={disabled}
69✔
921
                    sx={{ mb: "1rem" }}
69✔
922
                  />
923
                  {description && (
69✔
924
                    <FieldTooltip
33✔
925
                      title={<DisplayDescription description={description} />}
33✔
926
                      placement="right"
33✔
927
                      arrow
33✔
928
                      describeChild
33✔
929
                    >
930
                      <TooltipIcon />
33✔
931
                    </FieldTooltip>
33✔
932
                  )}
933
                </BaselineDiv>
69✔
934
              )
935
            }}
69✔
936
            name={name}
68✔
937
            control={control}
68✔
938
            defaultValue={defaultValue}
68✔
939
            rules={{ required: required }}
68✔
940
          />
941
        )
942
      }}
68✔
943
    </ConnectForm>
68✔
944
  )
945
}
68✔
946

947
/*
948
 * FormSelectField is rendered for selection from options where it's possible to choose many options
949
 */
950

951
const FormSelectField = ({
1✔
952
  name,
6✔
953
  label,
6✔
954
  required,
6✔
955
  options,
6✔
956
  description,
6✔
957
}: FormSelectFieldProps & { description: string }) => (
6✔
958
  <ConnectForm>
6✔
959
    {({ control }: ConnectFormMethods) => {
6✔
960
      return (
6✔
961
        <Controller
6✔
962
          name={name}
6✔
963
          control={control}
6✔
964
          render={({ field, fieldState: { error } }) => {
6✔
965
            return (
7✔
966
              <BaselineDiv>
7✔
967
                <TextField
7✔
968
                  {...field}
7✔
969
                  label={label}
7✔
970
                  id={name}
7✔
971
                  value={field.value || ""}
7✔
972
                  error={!!error}
7✔
973
                  helperText={error?.message}
7!
974
                  required={required}
7✔
975
                  select
7✔
976
                  SelectProps={{ native: true }}
7✔
977
                  onChange={e => {
7✔
978
                    let val = e.target.value
×
979
                    // Case: linkingAccessionIds which include "AccessionId + Form's title", we need to return only accessionId as value
980
                    if (val?.includes("Title")) {
×
981
                      const hyphenIndex = val.indexOf("-")
×
982
                      val = val.slice(0, hyphenIndex - 1)
×
983
                    }
×
984
                    return field.onChange(val)
×
985
                  }}
×
986
                  inputProps={{ "data-testid": name }}
7✔
987
                  sx={{ mb: "1rem" }}
7✔
988
                >
989
                  <option aria-label="None" value="" disabled />
7✔
990
                  {options.map(option => (
7✔
991
                    <option key={`${name}-${option}`} value={option} data-testid={`${name}-option`}>
21✔
992
                      {option}
21✔
993
                    </option>
21✔
994
                  ))}
7✔
995
                </TextField>
7✔
996
                {description && (
7!
997
                  <FieldTooltip
×
998
                    title={<DisplayDescription description={description} />}
×
999
                    placement="right"
×
1000
                    arrow
×
1001
                    describeChild
×
1002
                  >
1003
                    <TooltipIcon />
×
1004
                  </FieldTooltip>
×
1005
                )}
1006
              </BaselineDiv>
7✔
1007
            )
1008
          }}
7✔
1009
        />
1010
      )
1011
    }}
6✔
1012
  </ConnectForm>
6✔
1013
)
1014

1015
/*
1016
 * FormDatePicker used for selecting date or date rage in DOI form
1017
 */
1018

1019
const FormDatePicker = ({
1✔
1020
  name,
×
1021
  label,
×
1022
  required,
×
1023
  description,
×
1024
}: FormFieldBaseProps & { description: string }) => {
×
1025
  const dateCheckboxStyles = {
×
1026
    padding: 0,
×
1027
    margin: 0,
×
1028
    "& span.MuiTypography-root.MuiFormControlLabel-label": {
×
1029
      margin: 0,
×
1030
    },
×
1031
    "& span.MuiCheckbox-root": {
×
1032
      padding: 0,
×
1033
    },
×
1034
  }
×
1035

1036
  const [startDate, setStartDate] = useState("")
×
1037
  const [endDate, setEndDate] = useState("")
×
1038

1039
  const [unknownDates, setUnknownDates] = useState({
×
1040
    checkedStartDate: false,
×
1041
    checkedEndDate: false,
×
1042
  })
×
1043
  // Control calendar dialog opened
1044
  const [openStartCalendar, setOpenStartCalendar] = useState(false)
×
1045
  const [openEndCalendar, setOpenEndCalendar] = useState(false)
×
1046

1047
  return (
×
1048
    <ConnectForm>
×
1049
      {({ errors, getValues, setValue }: ConnectFormMethods) => {
×
1050
        const dateInputValues = getValues(name)
×
1051

1052
        const formatDate = (date: moment.MomentInput) => {
×
1053
          const format = "YYYY-MM-DD"
×
1054
          return moment(date).format(format)
×
1055
        }
×
1056

1057
        const getDateValues = (start: string, end: string) => {
×
1058
          if (start === end) return start
×
1059
          if (start || end) return `${start}/${end}`
×
1060
          else return ""
×
1061
        }
×
1062

1063
        const handleChangeStartDate = (e: moment.MomentInput) => {
×
1064
          const start = formatDate(e)
×
1065
          setStartDate(start)
×
1066
          const dateValues = getDateValues(start, endDate)
×
1067
          setValue(name, dateValues)
×
1068
        }
×
1069

1070
        const handleChangeEndDate = (e: moment.MomentInput) => {
×
1071
          const end = formatDate(e)
×
1072
          setEndDate(end)
×
1073
          const dateValues = getDateValues(startDate, end)
×
1074
          setValue(name, dateValues)
×
1075
        }
×
1076

1077
        const handleChangeUnknownDates = (e: { target: { name: string; checked: boolean } }) => {
×
1078
          setUnknownDates({ ...unknownDates, [e.target.name]: e.target.checked })
×
1079
          if (e.target.name === "checkedStartDate") {
×
1080
            setStartDate("")
×
1081
            const dateValues = getDateValues("", endDate)
×
1082
            setValue(name, dateValues)
×
1083
          } else {
×
1084
            setEndDate("")
×
1085
            const dateValues = getDateValues(startDate, "")
×
1086
            setValue(name, dateValues)
×
1087
          }
×
1088
        }
×
1089

1090
        const handleClearDates = () => {
×
1091
          setStartDate("")
×
1092
          setEndDate("")
×
1093
          setValue(name, "")
×
1094
        }
×
1095

1096
        type DateSelectionProps = {
1097
          label: string
1098
          inputRef: React.LegacyRef<HTMLInputElement> | undefined
1099
          inputProps: JSX.IntrinsicAttributes &
1100
            React.ClassAttributes<HTMLInputElement> &
1101
            React.InputHTMLAttributes<HTMLInputElement>
1102
        }
1103

1104
        type InputPropsType = {
1105
          InputProps: {
1106
            endAdornment:
1107
              | boolean
1108
              | React.ReactChild
1109
              | React.ReactFragment
1110
              | React.ReactPortal
1111
              | null
1112
              | undefined
1113
          }
1114
        }
1115

1116
        type DateSelectionPropsWithInput = DateSelectionProps & InputPropsType
1117

1118
        const DateSelection = (props: DateSelectionPropsWithInput) => {
×
1119
          return (
×
1120
            <Box sx={{ display: "flex", alignItems: "center" }} data-testid={props.label}>
×
1121
              <input
×
1122
                ref={props.inputRef}
×
1123
                {...props.inputProps}
×
1124
                style={{ border: "none", width: 0 }}
×
1125
              />
1126
              {props.InputProps?.endAdornment}
×
1127
            </Box>
×
1128
          )
1129
        }
×
1130

1131
        const DatePickerComponent = () => (
×
1132
          <LocalizationProvider dateAdapter={DateAdapter}>
×
1133
            <Box
×
1134
              sx={{
×
1135
                width: "95%",
×
1136
                display: "flex",
×
1137
                flexDirection: "row",
×
1138
                alignItems: "center",
×
1139
              }}
×
1140
            >
1141
              <BaselineDiv>
×
1142
                <TextField
×
1143
                  inputProps={{ "data-testid": name }}
×
1144
                  label={label}
×
1145
                  id={name}
×
1146
                  role="textbox"
×
1147
                  error={!!errors}
×
1148
                  helperText={errors?.message?.toString()}
×
1149
                  placeholder="YYYY-MM-DD"
×
1150
                  required={required}
×
1151
                  type="text"
×
1152
                  defaultValue={dateInputValues}
×
1153
                  InputProps={{
×
1154
                    endAdornment: (
×
1155
                      <IconButton
×
1156
                        onClick={handleClearDates}
×
1157
                        sx={{ position: "absolute", right: "-1", padding: 0 }}
×
1158
                      >
1159
                        <ClearIcon fontSize="small" />
×
1160
                      </IconButton>
×
1161
                    ),
1162
                  }}
×
1163
                />
1164
                {description && (
×
1165
                  <FieldTooltip
×
1166
                    title={<DisplayDescription description={description} />}
×
1167
                    placement="right"
×
1168
                    arrow
×
1169
                    describeChild
×
1170
                  >
1171
                    <TooltipIcon />
×
1172
                  </FieldTooltip>
×
1173
                )}
1174
              </BaselineDiv>
×
1175
              <Grid container direction="column">
×
1176
                <DatePickerWrapper>
×
1177
                  <span>Start</span>
×
1178
                  <FormControlLabel
×
1179
                    control={
×
1180
                      <Checkbox
×
1181
                        checked={unknownDates.checkedStartDate}
×
1182
                        onChange={handleChangeUnknownDates}
×
1183
                        name="checkedStartDate"
×
1184
                        value="UnknownStartDate"
×
1185
                        color="primary"
×
1186
                      />
1187
                    }
1188
                    label={<DateCheckboxLabel>Unknown</DateCheckboxLabel>}
×
1189
                    sx={{ ...dateCheckboxStyles }}
×
1190
                    disabled={unknownDates.checkedEndDate}
×
1191
                    data-testid="startDateCheck"
×
1192
                  />
1193
                  <DatePicker
×
1194
                    label="Start"
×
1195
                    renderInput={params => (
×
1196
                      <DateSelection
×
1197
                        {...(params as DateSelectionProps)}
×
1198
                        InputProps={{
×
1199
                          ...params.InputProps,
×
1200
                          endAdornment: (
×
1201
                            <InputAdornment
×
1202
                              position="end"
×
1203
                              onClick={() => setOpenStartCalendar(true)}
×
1204
                            >
1205
                              <IconButton disabled={unknownDates.checkedStartDate}>
×
1206
                                <EventIcon color="primary" />
×
1207
                              </IconButton>
×
1208
                            </InputAdornment>
×
1209
                          ),
1210
                        }}
×
1211
                      />
1212
                    )}
1213
                    value={startDate}
×
1214
                    onChange={handleChangeStartDate}
×
1215
                    minDate={moment("0001-01-01")}
×
1216
                    shouldDisableDate={day => moment(day).isAfter(endDate)}
×
1217
                    open={openStartCalendar}
×
1218
                    disableCloseOnSelect={false}
×
1219
                    onOpen={() => setOpenStartCalendar(true)}
×
1220
                    onClose={() => setOpenStartCalendar(false)}
×
1221
                  />
1222
                </DatePickerWrapper>
×
1223
                <DatePickerWrapper>
×
1224
                  <span>End</span>
×
1225
                  <FormControlLabel
×
1226
                    control={
×
1227
                      <Checkbox
×
1228
                        checked={unknownDates.checkedEndDate}
×
1229
                        onChange={handleChangeUnknownDates}
×
1230
                        name="checkedEndDate"
×
1231
                        value="UnknownEndDate"
×
1232
                        color="primary"
×
1233
                      />
1234
                    }
1235
                    label={<DateCheckboxLabel>Unknown</DateCheckboxLabel>}
×
1236
                    sx={{ ...dateCheckboxStyles }}
×
1237
                    disabled={unknownDates.checkedStartDate}
×
1238
                    data-testid="endDateCheck"
×
1239
                  />
1240
                  <DatePicker
×
1241
                    label="End"
×
1242
                    renderInput={params => (
×
1243
                      <DateSelection
×
1244
                        {...(params as DateSelectionProps)}
×
1245
                        InputProps={{
×
1246
                          ...params.InputProps,
×
1247
                          endAdornment: (
×
1248
                            <InputAdornment position="end" onClick={() => setOpenEndCalendar(true)}>
×
1249
                              <IconButton disabled={unknownDates.checkedEndDate}>
×
1250
                                <EventIcon color="primary" />
×
1251
                              </IconButton>
×
1252
                            </InputAdornment>
×
1253
                          ),
1254
                        }}
×
1255
                      />
1256
                    )}
1257
                    value={endDate}
×
1258
                    onChange={handleChangeEndDate}
×
1259
                    shouldDisableDate={day => moment(day).isBefore(startDate)}
×
1260
                    disableCloseOnSelect={false}
×
1261
                    open={openEndCalendar}
×
1262
                    onOpen={() => setOpenEndCalendar(true)}
×
1263
                    onClose={() => setOpenEndCalendar(false)}
×
1264
                  />
1265
                </DatePickerWrapper>
×
1266
              </Grid>
×
1267
            </Box>
×
1268
          </LocalizationProvider>
×
1269
        )
1270

1271
        return <DatePickerComponent />
×
1272
      }}
×
1273
    </ConnectForm>
×
1274
  )
1275
}
×
1276

1277
/*
1278
 * FormAutocompleteField uses ROR API to fetch organisations
1279
 */
1280
type RORItem = {
1281
  name: string
1282
  id: string
1283
}
1284

1285
const StyledAutocomplete = styled(Autocomplete)(() => ({
1✔
1286
  flex: "auto",
8✔
1287
  alignSelf: "flex-start",
8✔
1288
  "& + svg": {
8✔
1289
    marginTop: 1,
8✔
1290
  },
8✔
1291
})) as typeof Autocomplete
1✔
1292

1293
const FormAutocompleteField = ({
1✔
1294
  name,
9✔
1295
  label,
9✔
1296
  required,
9✔
1297
  description,
9✔
1298
}: FormFieldBaseProps & { description: string }) => {
9✔
1299
  const dispatch = useAppDispatch()
9✔
1300

1301
  const { setValue, getValues } = useFormContext()
9✔
1302

1303
  const defaultValue = getValues(name) || ""
9✔
1304
  const [selection, setSelection] = useState<RORItem | null>(null)
9✔
1305
  const [open, setOpen] = useState(false)
9✔
1306
  const [options, setOptions] = useState([])
9✔
1307
  const [inputValue, setInputValue] = useState("")
9✔
1308
  const [loading, setLoading] = useState(false)
9✔
1309

1310
  const fetchOrganisations = async (searchTerm: string) => {
9✔
1311
    // Check if searchTerm includes non-word char, for e.g. "(", ")", "-" because the api does not work with those chars
1312
    const isContainingNonWordChar = searchTerm.match(/\W/g)
1✔
1313
    const response =
1✔
1314
      isContainingNonWordChar === null ? await rorAPIService.getOrganisations(searchTerm) : null
1!
1315

1316
    if (response) setLoading(false)
1✔
1317

1318
    if (response?.ok) {
1✔
1319
      const mappedOrganisations = response.data.items.map((org: RORItem) => ({
1✔
1320
        name: org.name,
1✔
1321
        id: org.id,
1✔
1322
      }))
1✔
1323
      setOptions(mappedOrganisations)
1✔
1324
    }
1✔
1325
  }
1✔
1326

1327
  // Disable warning when using external function as callback
1328
  // https://stackoverflow.com/questions/62834368/react-usecallback-linting-error-missing-dependency
1329

1330
  const debouncedSearch = useCallback(
9✔
1331
    debounce((newInput: string) => {
9✔
1332
      if (newInput.length > 0) fetchOrganisations(newInput)
1✔
1333
    }, 150),
9✔
1334
    []
9✔
1335
  )
9✔
1336

1337
  useEffect(() => {
9✔
1338
    let active = true
4✔
1339

1340
    if (inputValue === "") {
4✔
1341
      setOptions([])
2✔
1342
      return undefined
2✔
1343
    }
2✔
1344

1345
    if (active && open) {
4✔
1346
      setLoading(true)
1✔
1347
      debouncedSearch(inputValue)
1✔
1348
    }
1✔
1349

1350
    return () => {
2✔
1351
      active = false
2✔
1352
      setLoading(false)
2✔
1353
    }
2✔
1354
  }, [selection, inputValue])
9✔
1355

1356
  return (
9✔
1357
    <ConnectForm>
9✔
1358
      {({ errors, control }: ConnectFormMethods) => {
9✔
1359
        const error = get(errors, name)
8✔
1360

1361
        const handleAutocompleteValueChange = (_event: unknown, option: RORItem) => {
8✔
1362
          setSelection(option)
1✔
1363
          setValue(name, option?.name)
1✔
1364
          option?.id ? dispatch(setAutocompleteField(option.id)) : null
1!
1365
        }
1✔
1366

1367
        const handleInputChange = (_event: unknown, newInputValue: string, reason: string) => {
8✔
1368
          setInputValue(newInputValue)
3✔
1369
          switch (reason) {
3✔
1370
            case "input":
3✔
1371
            case "clear":
3✔
1372
              setInputValue(newInputValue)
1✔
1373
              break
1✔
1374
            case "reset":
3✔
1375
              selection ? setInputValue(selection?.name) : null
1!
1376
              break
1✔
1377
            default:
3✔
1378
              break
1✔
1379
          }
3✔
1380
        }
3✔
1381

1382
        return (
8✔
1383
          <Controller
8✔
1384
            render={() => (
8✔
1385
              <StyledAutocomplete
8✔
1386
                freeSolo
8✔
1387
                open={open}
8✔
1388
                onOpen={() => {
8✔
1389
                  setOpen(true)
1✔
1390
                }}
1✔
1391
                onClose={() => {
8✔
1392
                  setOpen(false)
1✔
1393
                }}
1✔
1394
                options={options}
8✔
1395
                getOptionLabel={option => option.name || ""}
8✔
1396
                disableClearable={inputValue.length === 0}
8✔
1397
                renderInput={params => (
8✔
1398
                  <BaselineDiv>
10✔
1399
                    <TextField
10✔
1400
                      {...params}
10✔
1401
                      label={label}
10✔
1402
                      id={name}
10✔
1403
                      name={name}
10✔
1404
                      variant="outlined"
10✔
1405
                      error={!!error}
10✔
1406
                      required={required}
10✔
1407
                      InputProps={{
10✔
1408
                        ...params.InputProps,
10✔
1409
                        endAdornment: (
10✔
1410
                          <React.Fragment>
10✔
1411
                            {loading ? <CircularProgress color="inherit" size={20} /> : null}
10✔
1412
                            {params.InputProps.endAdornment}
10✔
1413
                          </React.Fragment>
10✔
1414
                        ),
1415
                      }}
10✔
1416
                      inputProps={{ ...params.inputProps, "data-testid": `${name}-inputField` }}
10✔
1417
                      sx={[
10✔
1418
                        {
10✔
1419
                          "&.MuiAutocomplete-endAdornment": {
10✔
1420
                            top: 0,
10✔
1421
                          },
10✔
1422
                        },
10✔
1423
                      ]}
10✔
1424
                    />
1425
                    {description && (
10!
1426
                      <FieldTooltip
×
1427
                        title={
×
1428
                          <DisplayDescription description={description}>
×
1429
                            <>
×
1430
                              <br />
×
1431
                              {"Organisations provided by "}
×
1432
                              <a href="https://ror.org/" target="_blank" rel="noreferrer">
×
1433
                                {"ror.org"}
×
1434
                                <LaunchIcon sx={{ fontSize: "1rem", mb: -1 }} />
×
1435
                              </a>
×
1436
                            </>
×
1437
                          </DisplayDescription>
×
1438
                        }
1439
                        placement="right"
×
1440
                        arrow
×
1441
                        describeChild
×
1442
                      >
1443
                        <TooltipIcon />
×
1444
                      </FieldTooltip>
×
1445
                    )}
1446
                  </BaselineDiv>
10✔
1447
                )}
1448
                onChange={handleAutocompleteValueChange}
8✔
1449
                onInputChange={handleInputChange}
8✔
1450
                value={defaultValue}
8✔
1451
                inputValue={inputValue || defaultValue}
8✔
1452
              />
1453
            )}
1454
            name={name}
8✔
1455
            control={control}
8✔
1456
          />
1457
        )
1458
      }}
8✔
1459
    </ConnectForm>
9✔
1460
  )
1461
}
9✔
1462

1463
const ValidationTagField = styled(TextField)(({ theme }) => ({
1✔
1464
  "& .MuiOutlinedInput-root.MuiInputBase-root": { flexWrap: "wrap" },
×
1465
  "& input": { flex: 1, minWidth: "2rem" },
×
1466
  "& label": { color: theme.palette.primary.main },
×
1467
  "& .MuiOutlinedInput-notchedOutline, div:hover .MuiOutlinedInput-notchedOutline":
×
1468
    highlightStyle(theme),
×
1469
}))
1✔
1470

1471
const FormTagField = ({
1✔
1472
  name,
×
1473
  label,
×
1474
  required,
×
1475
  description,
×
1476
}: FormFieldBaseProps & { description: string }) => {
×
1477
  // Check initialValues of the tag field and define the initialTags for rendering
1478
  const initialValues = useWatch({ name })
×
1479
  const initialTags: Array<string> = initialValues ? initialValues.split(",") : []
×
1480

1481
  const [inputValue, setInputValue] = React.useState("")
×
1482
  const [tags, setTags] = useState<Array<string>>(initialTags)
×
1483

1484
  const handleInputChange = e => {
×
1485
    setInputValue(e.target.value)
×
1486
  }
×
1487

1488
  return (
×
1489
    <ConnectForm>
×
1490
      {({ control }: ConnectFormMethods) => {
×
1491
        const defaultValue = initialValues || ""
×
1492
        return (
×
1493
          <Controller
×
1494
            name={name}
×
1495
            control={control}
×
1496
            defaultValue={defaultValue}
×
1497
            render={({ field }) => {
×
1498
              const handleKeywordAsTag = (keyword: string) => {
×
1499
                // newTags with unique values
1500
                const newTags = !tags.includes(keyword) ? [...tags, keyword] : tags
×
1501
                setTags(newTags)
×
1502
                setInputValue("")
×
1503
                // Convert tags to string for hidden registered input's values
1504
                field.onChange(newTags.join(","))
×
1505
              }
×
1506

1507
              const handleKeyDown = e => {
×
1508
                const { key } = e
×
1509
                const trimmedInput = inputValue.trim()
×
1510
                // Convert to tags if users press "," OR "Enter"
1511
                if ((key === "," || key === "Enter") && trimmedInput.length > 0) {
×
1512
                  e.preventDefault()
×
1513
                  handleKeywordAsTag(trimmedInput)
×
1514
                }
×
1515
              }
×
1516

1517
              // Convert to tags when user clicks outside of input field
1518
              const handleOnBlur = () => {
×
1519
                const trimmedInput = inputValue.trim()
×
1520
                if (trimmedInput.length > 0) {
×
1521
                  handleKeywordAsTag(trimmedInput)
×
1522
                }
×
1523
              }
×
1524

1525
              const handleTagDelete = item => () => {
×
1526
                const newTags = tags.filter(tag => tag !== item)
×
1527
                setTags(newTags)
×
1528
                field.onChange(newTags.join(","))
×
1529
              }
×
1530

1531
              return (
×
1532
                <BaselineDiv>
×
1533
                  <input
×
1534
                    {...field}
×
1535
                    required={required}
×
1536
                    style={{ width: 0, opacity: 0, transform: "translate(8rem, 2rem)" }}
×
1537
                  />
1538
                  <ValidationTagField
×
1539
                    InputProps={{
×
1540
                      startAdornment:
×
1541
                        tags.length > 0
×
1542
                          ? tags.map(item => (
×
1543
                              <Chip
×
1544
                                key={item}
×
1545
                                tabIndex={-1}
×
1546
                                label={item}
×
1547
                                onDelete={handleTagDelete(item)}
×
1548
                                color="primary"
×
1549
                                deleteIcon={<ClearIcon fontSize="small" />}
×
1550
                                data-testid={item}
×
1551
                                sx={{ fontSize: "1.4rem", m: "0.5rem" }}
×
1552
                              />
1553
                            ))
×
1554
                          : null,
×
1555
                    }}
×
1556
                    inputProps={{ "data-testid": name }}
×
1557
                    label={label}
×
1558
                    id={name}
×
1559
                    value={inputValue}
×
1560
                    onChange={handleInputChange}
×
1561
                    onKeyDown={handleKeyDown}
×
1562
                    onBlur={handleOnBlur}
×
1563
                  />
1564

1565
                  {description && (
×
1566
                    <FieldTooltip
×
1567
                      title={<DisplayDescription description={description} />}
×
1568
                      placement="right"
×
1569
                      arrow
×
1570
                      describeChild
×
1571
                    >
1572
                      <TooltipIcon />
×
1573
                    </FieldTooltip>
×
1574
                  )}
1575
                </BaselineDiv>
×
1576
              )
1577
            }}
×
1578
          />
1579
        )
1580
      }}
×
1581
    </ConnectForm>
×
1582
  )
1583
}
×
1584

1585
/*
1586
 * Highlight required Checkbox
1587
 */
1588
const ValidationFormControlLabel = styled(FormControlLabel)(({ theme }) => ({
1✔
1589
  label: {
6✔
1590
    "& span": { color: theme.palette.primary.main },
6✔
1591
  },
6✔
1592
}))
1✔
1593

1594
const FormBooleanField = ({
1✔
1595
  name,
6✔
1596
  label,
6✔
1597
  required,
6✔
1598
  description,
6✔
1599
}: FormFieldBaseProps & { description: string }) => {
6✔
1600
  return (
6✔
1601
    <ConnectForm>
6✔
1602
      {({ register, errors, getValues }: ConnectFormMethods) => {
6✔
1603
        const error = get(errors, name)
6✔
1604

1605
        const { ref, ...rest } = register(name)
6✔
1606
        // DAC form: "values" of MainContact checkbox
1607
        const values = getValues(name)
6✔
1608
        return (
6✔
1609
          <Box display="inline" px={1}>
6✔
1610
            <FormControl error={!!error} required={required}>
6✔
1611
              <FormGroup>
6✔
1612
                <BaselineDiv>
6✔
1613
                  <ValidationFormControlLabel
6✔
1614
                    control={
6✔
1615
                      <Checkbox
6✔
1616
                        id={name}
6✔
1617
                        {...rest}
6✔
1618
                        name={name}
6✔
1619
                        required={required}
6✔
1620
                        inputRef={ref}
6✔
1621
                        color="primary"
6✔
1622
                        checked={values || false}
6✔
1623
                        inputProps={
6✔
1624
                          { "data-testid": name } as React.InputHTMLAttributes<HTMLInputElement>
6✔
1625
                        }
6✔
1626
                      />
1627
                    }
1628
                    label={
6✔
1629
                      <label>
6✔
1630
                        {label}
6✔
1631
                        <span>{required ? ` * ` : ""}</span>
6!
1632
                      </label>
6✔
1633
                    }
6✔
1634
                  />
1635
                  {description && (
6!
1636
                    <FieldTooltip
×
1637
                      title={<DisplayDescription description={description} />}
×
1638
                      placement="right"
×
1639
                      arrow
×
1640
                      describeChild
×
1641
                    >
1642
                      <TooltipIcon />
×
1643
                    </FieldTooltip>
×
1644
                  )}
1645
                </BaselineDiv>
6✔
1646

1647
                <FormHelperText>{error?.message}</FormHelperText>
6!
1648
              </FormGroup>
6✔
1649
            </FormControl>
6✔
1650
          </Box>
6✔
1651
        )
1652
      }}
6✔
1653
    </ConnectForm>
6✔
1654
  )
1655
}
6✔
1656

1657
const FormCheckBoxArray = ({
1✔
1658
  name,
6✔
1659
  label,
6✔
1660
  required,
6✔
1661
  options,
6✔
1662
  description,
6✔
1663
}: FormSelectFieldProps & { description: string }) => (
6✔
1664
  <Box px={1}>
6✔
1665
    <p>{label} Check from following options</p>
6✔
1666
    <ConnectForm>
6✔
1667
      {({ register, errors, getValues }: ConnectFormMethods) => {
6✔
1668
        const values = getValues()[name]
6✔
1669

1670
        const error = get(errors, name)
6✔
1671

1672
        const { ref, ...rest } = register(name)
6✔
1673

1674
        return (
6✔
1675
          <FormControl error={!!error} required={required}>
6✔
1676
            <FormGroup aria-labelledby={name}>
6✔
1677
              {options.map(option => (
6✔
1678
                <React.Fragment key={option}>
12✔
1679
                  <FormControlLabel
12✔
1680
                    key={option}
12✔
1681
                    control={
12✔
1682
                      <Checkbox
12✔
1683
                        {...rest}
12✔
1684
                        inputRef={ref}
12✔
1685
                        name={name}
12✔
1686
                        value={option}
12✔
1687
                        checked={values && values?.includes(option) ? true : false}
12!
1688
                        color="primary"
12✔
1689
                        defaultValue=""
12✔
1690
                        inputProps={
12✔
1691
                          { "data-testid": name } as React.InputHTMLAttributes<HTMLInputElement>
12✔
1692
                        }
12✔
1693
                      />
1694
                    }
1695
                    label={option}
12✔
1696
                  />
1697
                  {description && (
12!
1698
                    <FieldTooltip
×
1699
                      title={<DisplayDescription description={description} />}
×
1700
                      placement="right"
×
1701
                      arrow
×
1702
                      describeChild
×
1703
                    >
1704
                      <TooltipIcon />
×
1705
                    </FieldTooltip>
×
1706
                  )}
1707
                </React.Fragment>
12✔
1708
              ))}
6✔
1709
              <FormHelperText>{error?.message}</FormHelperText>
6!
1710
            </FormGroup>
6✔
1711
          </FormControl>
6✔
1712
        )
1713
      }}
6✔
1714
    </ConnectForm>
6✔
1715
  </Box>
6✔
1716
)
1717

1718
type FormArrayProps = {
1719
  object: FormObject
1720
  path: Array<string>
1721
  required: boolean
1722
}
1723

1724
const FormArrayTitle = styled(Paper, { shouldForwardProp: prop => prop !== "level" })<{
1✔
1725
  level: number
1726
}>(({ theme, level }) => ({
1✔
1727
  display: "flex",
11✔
1728
  flexDirection: "column",
11✔
1729
  justifyContent: "start",
11✔
1730
  alignItems: "start",
11✔
1731
  backgroundColor: level < 2 ? theme.palette.primary.lighter : theme.palette.common.white,
11!
1732
  height: "100%",
11✔
1733
  marginLeft: level < 2 ? "5rem" : 0,
11!
1734
  marginRight: "3rem",
11✔
1735
  padding: level === 1 ? "2rem" : 0,
11!
1736
}))
1✔
1737

1738
const FormArrayChildrenTitle = styled(Paper)(() => ({
1✔
1739
  width: "70%",
1✔
1740
  display: "inline-block",
1✔
1741
  marginBottom: "1rem",
1✔
1742
  paddingLeft: "1rem",
1✔
1743
  paddingTop: "1rem",
1✔
1744
}))
1✔
1745

1746
/*
1747
 * FormArray is rendered for arrays of objects. User is given option to choose how many objects to add to array.
1748
 */
1749
const FormArray = ({
1✔
1750
  object,
11✔
1751
  path,
11✔
1752
  required,
11✔
1753
  description,
11✔
1754
}: FormArrayProps & { description: string }) => {
11✔
1755
  const name = pathToName(path)
11✔
1756
  const [lastPathItem] = path.slice(-1)
11✔
1757
  const level = path.length
11✔
1758
  const label = object.title ?? lastPathItem
11!
1759

1760
  // Get currentObject and the values of current field
1761
  const currentObject = useAppSelector(state => state.currentObject) || {}
11!
1762
  const fileTypes = useAppSelector(state => state.fileTypes)
11✔
1763

1764
  const fieldValues = get(currentObject, name)
11✔
1765

1766
  const items = traverseValues(object.items) as FormObject
11✔
1767

1768
  const { control } = useForm()
11✔
1769

1770
  const {
11✔
1771
    register,
11✔
1772
    getValues,
11✔
1773
    setValue,
11✔
1774
    formState: { isSubmitted },
11✔
1775
    clearErrors,
11✔
1776
  } = useFormContext()
11✔
1777

1778
  const { fields, append, remove } = useFieldArray({ control, name })
11✔
1779

1780
  const [isValid, setValid] = React.useState(false)
11✔
1781
  const [formFields, setFormFields] = useState<Record<"id", string>[] | null>(null)
11✔
1782

1783
  // Append the correct values to the equivalent fields when editing form
1784
  // This applies for the case: "fields" does not get the correct data (empty array) although there are values in the fields
1785
  // E.g. Study > StudyLinks or Experiment > Expected Base Call Table
1786
  // Append only once when form is populated
1787
  useEffect(() => {
11✔
1788
    if (
4✔
1789
      fieldValues?.length > 0 &&
4!
1790
      fields?.length === 0 &&
×
1791
      typeof fieldValues === "object" &&
×
1792
      !formFields
×
1793
    ) {
4!
1794
      const fieldsArray: Record<string, unknown>[] = []
×
1795
      for (let i = 0; i < fieldValues.length; i += 1) {
×
1796
        fieldsArray.push({ fieldValues: fieldValues[i] })
×
1797
      }
×
1798
      append(fieldsArray)
×
1799
    }
×
1800
    // Create initial fields when editing object
1801
    setFormFields(fields)
4✔
1802
  }, [fields])
11✔
1803

1804
  // Get unique fileTypes from submitted fileTypes
1805
  const uniqueFileTypes = uniq(
11✔
1806
    flatten(fileTypes?.map((obj: { fileTypes: string[] }) => obj.fileTypes))
11✔
1807
  )
11✔
1808

1809
  useEffect(() => {
11✔
1810
    // Append fileType to formats' field
1811
    if (name === "formats") {
3!
1812
      for (let i = 0; i < uniqueFileTypes.length; i += 1) {
×
1813
        append({ formats: uniqueFileTypes[i] })
×
1814
      }
×
1815
    }
×
1816
  }, [uniqueFileTypes.length])
11✔
1817

1818
  // Clear required field array error and append
1819
  const handleAppend = () => {
11✔
1820
    setValid(true)
1✔
1821
    clearErrors([name])
1✔
1822
    append({})
1✔
1823
  }
1✔
1824

1825
  const handleRemove = (index: number) => {
11✔
1826
    // Re-register hidden input if all field arrays are removed
1827
    if (index === 0) setValid(false)
×
1828
    // Set the correct values according to the name path when removing a field
1829
    const values = getValues(name)
×
1830
    const filteredValues = values?.filter((_val: unknown, ind: number) => ind !== index)
×
1831
    setValue(name, filteredValues)
×
1832
    setFormFields(filteredValues)
×
1833
    remove(index)
×
1834
  }
×
1835

1836
  return (
11✔
1837
    <Grid
11✔
1838
      container
11✔
1839
      key={`${name}-array`}
11✔
1840
      aria-labelledby={name}
11✔
1841
      data-testid={name}
11✔
1842
      direction={level < 2 ? "row" : "column"}
11!
1843
      sx={{ mb: level === 1 ? "3rem" : 0 }}
11!
1844
    >
1845
      <Grid size={{ xs: 12, md: 4 }}>
11✔
1846
        {
1847
          <FormArrayTitle square={true} elevation={0} level={level}>
11✔
1848
            {required && !isValid && (
11!
1849
              <input hidden={true} value="form-array-required" {...register(name)} />
×
1850
            )}
1851
            <Typography
11✔
1852
              key={`${name}-header`}
11✔
1853
              variant={"subtitle1"}
11✔
1854
              data-testid={name}
11✔
1855
              role="heading"
11✔
1856
              color="secondary"
11✔
1857
            >
1858
              {label}
11✔
1859
              {required ? "*" : null}
11!
1860
              {required && !isValid && formFields?.length === 0 && isSubmitted && (
11!
1861
                <span>
×
1862
                  <FormControl error>
×
1863
                    <FormHelperText>must have at least 1 item</FormHelperText>
×
1864
                  </FormControl>
×
1865
                </span>
×
1866
              )}
1867
              {description && (
11!
1868
                <FieldTooltip
×
1869
                  title={<DisplayDescription description={description} />}
×
1870
                  placement="top"
×
1871
                  arrow
×
1872
                  describeChild
×
1873
                >
1874
                  <TooltipIcon />
×
1875
                </FieldTooltip>
×
1876
              )}
1877
            </Typography>
11✔
1878
          </FormArrayTitle>
11✔
1879
        }
1880
      </Grid>
11✔
1881

1882
      <Grid size={{ xs: 12, md: 8 }}>
11✔
1883
        {formFields?.map((field, index) => {
11✔
1884
          const pathWithoutLastItem = path.slice(0, -1)
1✔
1885
          const lastPathItemWithIndex = `${lastPathItem}.${index}`
1✔
1886

1887
          if (items.oneOf) {
1✔
1888
            const pathForThisIndex = [...pathWithoutLastItem, lastPathItemWithIndex]
1✔
1889

1890
            return (
1✔
1891
              <Box
1✔
1892
                key={field.id || index}
1✔
1893
                data-testid={`${name}[${index}]`}
1✔
1894
                display="flex"
1✔
1895
                alignItems="center"
1✔
1896
              >
1897
                <FormArrayChildrenTitle elevation={2} square>
1✔
1898
                  <FormOneOfField
1✔
1899
                    key={field.id}
1✔
1900
                    nestedField={field as NestedField}
1✔
1901
                    path={pathForThisIndex}
1✔
1902
                    object={items}
1✔
1903
                  />
1904
                </FormArrayChildrenTitle>
1✔
1905
                <IconButton onClick={() => handleRemove(index)}>
1✔
1906
                  <RemoveIcon />
1✔
1907
                </IconButton>
1!
1908
              </Box>
1✔
1909
            )
1910
          }
1!
1911

1912
          const properties = object.items.properties
×
1913
          let requiredProperties =
×
1914
            index === 0 && object.contains?.allOf
×
1915
              ? object.contains?.allOf?.flatMap((item: FormObject) => item.required) // Case: DAC - Main Contact needs at least 1
×
1916
              : object.items?.required
×
1917

1918
          // Force first array item as required field if array is required but none of the items are required
1919
          if (required && !requiredProperties) requiredProperties = [Object.keys(items)[0]]
1!
1920

1921
          return (
×
1922
            <Box key={field.id || index} aria-labelledby={name} display="flex" alignItems="center">
×
1923
              <FormArrayChildrenTitle elevation={2} square>
×
1924
                {
1925
                  items
×
1926
                    ? Object.keys(items).map(item => {
×
1927
                        const pathForThisIndex = [
×
1928
                          ...pathWithoutLastItem,
×
1929
                          lastPathItemWithIndex,
×
1930
                          item,
×
1931
                        ]
1932
                        const requiredField = requiredProperties
×
1933
                          ? requiredProperties.filter((prop: string) => prop === item)
×
1934
                          : []
×
1935
                        return traverseFields(
×
1936
                          properties[item] as FormObject,
×
1937
                          pathForThisIndex,
×
1938
                          requiredField,
×
1939
                          false,
×
NEW
1940
                          field as NestedField
×
1941
                        )
×
1942
                      })
×
1943
                    : traverseFields(
×
1944
                        object.items,
×
1945
                        [...pathWithoutLastItem, lastPathItemWithIndex],
×
1946
                        [],
×
1947
                        false,
×
NEW
1948
                        field as NestedField
×
1949
                      ) // special case for doiSchema's "sizes" and "formats"
×
1950
                }
1951
              </FormArrayChildrenTitle>
1✔
1952
              <IconButton onClick={() => handleRemove(index)} size="large">
1✔
1953
                <RemoveIcon />
1✔
1954
              </IconButton>
1!
1955
            </Box>
1✔
1956
          )
1957
        })}
11✔
1958

1959
        <Button
11✔
1960
          variant="contained"
11✔
1961
          color="primary"
11✔
1962
          size="small"
11✔
1963
          startIcon={<AddIcon />}
11✔
1964
          onClick={() => handleAppend()}
11✔
1965
          sx={{ mb: "1rem" }}
11✔
1966
        >
1967
          Add new item
1968
        </Button>
11✔
1969
      </Grid>
11✔
1970
    </Grid>
11✔
1971
  )
1972
}
11✔
1973

1974
export default {
1✔
1975
  buildFields,
1✔
1976
  cleanUpFormValues,
1✔
1977
}
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

© 2026 Coveralls, Inc