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

CSCfi / metadata-submitter-frontend / 15271191055

27 May 2025 09:05AM UTC coverage: 57.382% (+0.1%) from 57.253%
15271191055

push

github

Hang Le
Update React to version 19 (merge commit)

Merge branch 'feature/upgrade-react-v19' into 'main'
* Fix styles and Home view's filter error

* Update React to version 19

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

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

645 of 927 branches covered (69.58%)

Branch coverage included in aggregate %.

78 of 107 new or added lines in 19 files covered. (72.9%)

4 existing lines in 2 files now uncovered.

6149 of 10913 relevant lines covered (56.35%)

4.81 hits per line

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

62.41
/src/components/SubmissionWizard/WizardForms/WizardJSONSchemaParser.tsx
1
import * as React from "react"
1✔
2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

631
  const name = pathToName(path)
9✔
632

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

817
  let disabled = false // boolean if inputValue is disabled
59✔
818

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
1035
  const [startDate, setStartDate] = React.useState("")
×
NEW
1036
  const [endDate, setEndDate] = React.useState("")
×
1037

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

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

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

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

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

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

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

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

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

1103
        type InputPropsType = {
1104
          InputProps: {
1105
            endAdornment:
1106
              | boolean
1107
              | React.ReactElement<unknown>
1108
              | number
1109
              | string
1110
              | Iterable<React.ReactNode>
1111
              | React.ReactPortal
1112
              | null
1113
              | undefined
1114
          }
1115
        }
1116

1117
        type DateSelectionPropsWithInput = DateSelectionProps & InputPropsType
1118

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

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

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

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

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

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

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

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

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

1318
    if (response) setLoading(false)
1✔
1319

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

1329
  const debouncedSearch = debounce((newInput: string) => {
9✔
1330
    if (newInput.length > 0) fetchOrganisations(newInput)
1✔
1331
  }, 150)
9✔
1332

1333
  React.useEffect(() => {
9✔
1334
    let active = true
4✔
1335

1336
    if (inputValue === "") {
4✔
1337
      setOptions([])
2✔
1338
      return undefined
2✔
1339
    }
2✔
1340

1341
    if (active && open) {
4✔
1342
      setLoading(true)
1✔
1343
      debouncedSearch(inputValue)
1✔
1344
    }
1✔
1345

1346
    return () => {
2✔
1347
      active = false
2✔
1348
      setLoading(false)
2✔
1349
    }
2✔
1350
  }, [selection, inputValue])
9✔
1351

1352
  React.useEffect(() => {
9✔
1353
    if (clearForm) {
2!
1354
      setSelection(null)
×
1355
      setInputValue("")
×
1356
    }
×
1357
  }, [clearForm])
9✔
1358

1359
  return (
9✔
1360
    <ConnectForm>
9✔
1361
      {({ errors, control }: ConnectFormMethods) => {
9✔
1362
        const error = get(errors, name)
8✔
1363

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

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

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

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

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

1484
  const [inputValue, setInputValue] = React.useState("")
×
NEW
1485
  const [tags, setTags] = React.useState<Array<string>>(initialTags)
×
1486

1487
  const handleInputChange = e => {
×
1488
    setInputValue(e.target.value)
×
1489
  }
×
1490

1491
  const clearForm = useAppSelector(state => state.clearForm)
×
1492

NEW
1493
  React.useEffect(() => {
×
1494
    if (clearForm) {
×
1495
      setTags([])
×
1496
      setInputValue("")
×
1497
    }
×
1498
  }, [clearForm])
×
1499

1500
  return (
×
1501
    <ConnectForm>
×
1502
      {({ control }: ConnectFormMethods) => {
×
1503
        const defaultValue = initialValues || ""
×
1504
        return (
×
1505
          <Controller
×
1506
            name={name}
×
1507
            control={control}
×
1508
            defaultValue={defaultValue}
×
1509
            render={({ field }) => {
×
1510
              const handleKeywordAsTag = (keyword: string) => {
×
1511
                // newTags with unique values
1512
                const newTags = !tags.includes(keyword) ? [...tags, keyword] : tags
×
1513
                setTags(newTags)
×
1514
                setInputValue("")
×
1515
                // Convert tags to string for hidden registered input's values
1516
                field.onChange(newTags.join(","))
×
1517
              }
×
1518

1519
              const handleKeyDown = e => {
×
1520
                const { key } = e
×
1521
                const trimmedInput = inputValue.trim()
×
1522
                // Convert to tags if users press "," OR "Enter"
1523
                if ((key === "," || key === "Enter") && trimmedInput.length > 0) {
×
1524
                  e.preventDefault()
×
1525
                  handleKeywordAsTag(trimmedInput)
×
1526
                }
×
1527
              }
×
1528

1529
              // Convert to tags when user clicks outside of input field
1530
              const handleOnBlur = () => {
×
1531
                const trimmedInput = inputValue.trim()
×
1532
                if (trimmedInput.length > 0) {
×
1533
                  handleKeywordAsTag(trimmedInput)
×
1534
                }
×
1535
              }
×
1536

1537
              const handleTagDelete = item => () => {
×
1538
                const newTags = tags.filter(tag => tag !== item)
×
1539
                setTags(newTags)
×
1540
                field.onChange(newTags.join(","))
×
1541
              }
×
1542

1543
              return (
×
1544
                <BaselineDiv>
×
1545
                  <input
×
1546
                    {...field}
×
1547
                    required={required}
×
1548
                    style={{ width: 0, opacity: 0, transform: "translate(8rem, 2rem)" }}
×
1549
                  />
1550
                  <ValidationTagField
×
1551
                    InputProps={{
×
1552
                      startAdornment:
×
1553
                        tags.length > 0
×
1554
                          ? tags.map(item => (
×
1555
                              <Chip
×
1556
                                key={item}
×
1557
                                tabIndex={-1}
×
1558
                                label={item}
×
1559
                                onDelete={handleTagDelete(item)}
×
1560
                                color="primary"
×
1561
                                deleteIcon={<ClearIcon fontSize="small" />}
×
1562
                                data-testid={item}
×
1563
                                sx={{ fontSize: "1.4rem", m: "0.5rem" }}
×
1564
                              />
1565
                            ))
×
1566
                          : null,
×
1567
                    }}
×
1568
                    inputProps={{ "data-testid": name }}
×
1569
                    label={label}
×
1570
                    id={name}
×
1571
                    value={inputValue}
×
1572
                    onChange={handleInputChange}
×
1573
                    onKeyDown={handleKeyDown}
×
1574
                    onBlur={handleOnBlur}
×
1575
                  />
1576

1577
                  {description && (
×
1578
                    <FieldTooltip
×
1579
                      title={<DisplayDescription description={description} />}
×
1580
                      placement="right"
×
1581
                      arrow
×
1582
                      describeChild
×
1583
                    >
1584
                      <TooltipIcon />
×
1585
                    </FieldTooltip>
×
1586
                  )}
1587
                </BaselineDiv>
×
1588
              )
1589
            }}
×
1590
          />
1591
        )
1592
      }}
×
1593
    </ConnectForm>
×
1594
  )
1595
}
×
1596

1597
/*
1598
 * Highlight required Checkbox
1599
 */
1600
const ValidationFormControlLabel = styled(FormControlLabel)(({ theme }) => ({
1✔
1601
  label: {
6✔
1602
    "& span": { color: theme.palette.primary.main },
6✔
1603
  },
6✔
1604
}))
1✔
1605

1606
const FormBooleanField = ({
1✔
1607
  name,
6✔
1608
  label,
6✔
1609
  required,
6✔
1610
  description,
6✔
1611
}: FormFieldBaseProps & { description: string }) => {
6✔
1612
  return (
6✔
1613
    <ConnectForm>
6✔
1614
      {({ register, errors, getValues }: ConnectFormMethods) => {
6✔
1615
        const error = get(errors, name)
6✔
1616

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

1659
                <FormHelperText>{error?.message}</FormHelperText>
6!
1660
              </FormGroup>
6✔
1661
            </FormControl>
6✔
1662
          </Box>
6✔
1663
        )
1664
      }}
6✔
1665
    </ConnectForm>
6✔
1666
  )
1667
}
6✔
1668

1669
const FormCheckBoxArray = ({
1✔
1670
  name,
6✔
1671
  label,
6✔
1672
  required,
6✔
1673
  options,
6✔
1674
  description,
6✔
1675
}: FormSelectFieldProps & { description: string }) => (
6✔
1676
  <Box px={1}>
6✔
1677
    <p>{label} Check from following options</p>
6✔
1678
    <ConnectForm>
6✔
1679
      {({ register, errors, getValues }: ConnectFormMethods) => {
6✔
1680
        const values = getValues()[name]
6✔
1681

1682
        const error = get(errors, name)
6✔
1683

1684
        const { ref, ...rest } = register(name)
6✔
1685

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

1730
type FormArrayProps = {
1731
  object: FormObject
1732
  path: Array<string>
1733
  required: boolean
1734
}
1735

1736
const FormArrayTitle = styled(Paper, { shouldForwardProp: prop => prop !== "level" })<{
1✔
1737
  level: number
1738
}>(({ theme, level }) => ({
1✔
1739
  display: "flex",
8✔
1740
  flexDirection: "column",
8✔
1741
  justifyContent: "start",
8✔
1742
  alignItems: "start",
8✔
1743
  backgroundColor: level < 2 ? theme.palette.primary.light : theme.palette.common.white,
8!
1744
  height: "100%",
8✔
1745
  marginLeft: level < 2 ? "5rem" : 0,
8!
1746
  marginRight: "3rem",
8✔
1747
  padding: level === 1 ? "2rem" : 0,
8!
1748
}))
1✔
1749

1750
const FormArrayChildrenTitle = styled(Paper)(() => ({
1✔
1751
  width: "70%",
1✔
1752
  display: "inline-block",
1✔
1753
  marginBottom: "1rem",
1✔
1754
  paddingLeft: "1rem",
1✔
1755
  paddingTop: "1rem",
1✔
1756
}))
1✔
1757

1758
/*
1759
 * FormArray is rendered for arrays of objects. User is given option to choose how many objects to add to array.
1760
 */
1761
const FormArray = ({
1✔
1762
  object,
8✔
1763
  path,
8✔
1764
  required,
8✔
1765
  description,
8✔
1766
}: FormArrayProps & { description: string }) => {
8✔
1767
  const name = pathToName(path)
8✔
1768
  const [lastPathItem] = path.slice(-1)
8✔
1769
  const level = path.length
8✔
1770
  const label = object.title ?? lastPathItem
8!
1771

1772
  // Get currentObject and the values of current field
1773
  const currentObject = useAppSelector(state => state.currentObject) || {}
8!
1774
  const fileTypes = useAppSelector(state => state.fileTypes)
8✔
1775

1776
  const fieldValues = get(currentObject, name)
8✔
1777

1778
  const items = traverseValues(object.items) as FormObject
8✔
1779

1780
  const { control } = useForm()
8✔
1781

1782
  const {
8✔
1783
    register,
8✔
1784
    getValues,
8✔
1785
    setValue,
8✔
1786
    formState: { isSubmitted },
8✔
1787
    clearErrors,
8✔
1788
  } = useFormContext()
8✔
1789

1790
  const { fields, append, remove } = useFieldArray({ control, name })
8✔
1791

1792
  const [isValid, setValid] = React.useState(false)
8✔
1793
  const [formFields, setFormFields] = React.useState<Record<"id", string>[] | null>(null)
8✔
1794

1795
  // Append the correct values to the equivalent fields when editing form
1796
  // This applies for the case: "fields" does not get the correct data (empty array) although there are values in the fields
1797
  // E.g. Study > StudyLinks or Experiment > Expected Base Call Table
1798
  // Append only once when form is populated
1799
  React.useEffect(() => {
8✔
1800
    if (
4✔
1801
      fieldValues?.length > 0 &&
4!
1802
      fields?.length === 0 &&
×
1803
      typeof fieldValues === "object" &&
×
1804
      !formFields
×
1805
    ) {
4!
1806
      const fieldsArray: Record<string, unknown>[] = []
×
1807
      for (let i = 0; i < fieldValues.length; i += 1) {
×
1808
        fieldsArray.push({ fieldValues: fieldValues[i] })
×
1809
      }
×
1810
      append(fieldsArray)
×
1811
    }
×
1812
    // Create initial fields when editing object
1813
    setFormFields(fields)
4✔
1814
  }, [fields])
8✔
1815

1816
  // Get unique fileTypes from submitted fileTypes
1817
  const uniqueFileTypes = uniq(
8✔
1818
    flatten(fileTypes?.map((obj: { fileTypes: string[] }) => obj.fileTypes))
8✔
1819
  )
8✔
1820

1821
  React.useEffect(() => {
8✔
1822
    // Append fileType to formats' field
1823
    if (name === "formats") {
3!
1824
      for (let i = 0; i < uniqueFileTypes.length; i += 1) {
×
1825
        append({ formats: uniqueFileTypes[i] })
×
1826
      }
×
1827
    }
×
1828
  }, [uniqueFileTypes.length])
8✔
1829

1830
  // Clear required field array error and append
1831
  const handleAppend = () => {
8✔
1832
    setValid(true)
1✔
1833
    clearErrors([name])
1✔
1834
    append({})
1✔
1835
  }
1✔
1836

1837
  const handleRemove = (index: number) => {
8✔
1838
    // Re-register hidden input if all field arrays are removed
1839
    if (index === 0) setValid(false)
×
1840
    // Set the correct values according to the name path when removing a field
1841
    const values = getValues(name)
×
1842
    const filteredValues = values?.filter((_val: unknown, ind: number) => ind !== index)
×
1843
    setValue(name, filteredValues)
×
1844
    setFormFields(filteredValues)
×
1845
    remove(index)
×
1846
  }
×
1847

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

1894
      <Grid size={{ xs: 12, md: 8 }}>
8✔
1895
        {formFields?.map((field, index) => {
8✔
1896
          const pathWithoutLastItem = path.slice(0, -1)
1✔
1897
          const lastPathItemWithIndex = `${lastPathItem}.${index}`
1✔
1898

1899
          if (items.oneOf) {
1✔
1900
            const pathForThisIndex = [...pathWithoutLastItem, lastPathItemWithIndex]
1✔
1901

1902
            return (
1✔
1903
              <Box
1✔
1904
                key={field.id || index}
1✔
1905
                data-testid={`${name}[${index}]`}
1✔
1906
                display="flex"
1✔
1907
                alignItems="center"
1✔
1908
              >
1909
                <FormArrayChildrenTitle elevation={2} square>
1✔
1910
                  <FormOneOfField
1✔
1911
                    key={field.id}
1✔
1912
                    nestedField={field as NestedField}
1✔
1913
                    path={pathForThisIndex}
1✔
1914
                    object={items}
1✔
1915
                  />
1916
                </FormArrayChildrenTitle>
1✔
1917
                <IconButton onClick={() => handleRemove(index)}>
1✔
1918
                  <RemoveIcon />
1✔
1919
                </IconButton>
1!
1920
              </Box>
1✔
1921
            )
1922
          }
1!
1923

1924
          const properties = object.items.properties
×
1925
          let requiredProperties =
×
1926
            index === 0 && object.contains?.allOf
×
1927
              ? object.contains?.allOf?.flatMap((item: FormObject) => item.required) // Case: DAC - Main Contact needs at least 1
×
1928
              : object.items?.required
×
1929

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

1933
          return (
×
1934
            <Box key={field.id || index} aria-labelledby={name} display="flex" alignItems="center">
×
1935
              <FormArrayChildrenTitle elevation={2} square>
×
1936
                {
1937
                  items
×
1938
                    ? Object.keys(items).map(item => {
×
1939
                        const pathForThisIndex = [
×
1940
                          ...pathWithoutLastItem,
×
1941
                          lastPathItemWithIndex,
×
1942
                          item,
×
1943
                        ]
1944
                        const requiredField = requiredProperties
×
1945
                          ? requiredProperties.filter((prop: string) => prop === item)
×
1946
                          : []
×
1947
                        return traverseFields(
×
1948
                          properties[item] as FormObject,
×
1949
                          pathForThisIndex,
×
1950
                          requiredField,
×
1951
                          false,
×
1952
                          field as NestedField
×
1953
                        )
×
1954
                      })
×
1955
                    : traverseFields(
×
1956
                        object.items,
×
1957
                        [...pathWithoutLastItem, lastPathItemWithIndex],
×
1958
                        [],
×
1959
                        false,
×
1960
                        field as NestedField
×
1961
                      ) // special case for doiSchema's "sizes" and "formats"
×
1962
                }
1963
              </FormArrayChildrenTitle>
1✔
1964
              <IconButton onClick={() => handleRemove(index)} size="large">
1✔
1965
                <RemoveIcon />
1✔
1966
              </IconButton>
1!
1967
            </Box>
1✔
1968
          )
1969
        })}
8✔
1970

1971
        <Button
8✔
1972
          variant="contained"
8✔
1973
          color="primary"
8✔
1974
          size="small"
8✔
1975
          startIcon={<AddIcon />}
8✔
1976
          onClick={() => handleAppend()}
8✔
1977
          sx={{ mb: "1rem" }}
8✔
1978
        >
1979
          Add new item
1980
        </Button>
8✔
1981
      </Grid>
8✔
1982
    </Grid>
8✔
1983
  )
1984
}
8✔
1985

1986
export default {
1✔
1987
  buildFields,
1✔
1988
  cleanUpFormValues,
1✔
1989
}
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