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

CSCfi / metadata-submitter-frontend / 15903498740

26 Jun 2025 01:41PM UTC coverage: 57.696% (-0.1%) from 57.812%
15903498740

push

github

mradavi
Update all non-major dependencies (merge commit)

Merge branch 'renovate/all-minor-patch' into 'main'
* Update all non-major dependencies

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

Approved-by: Monika Radaviciute <mradavic@csc.fi>
Co-authored-by: renovate-bot <group_183_bot_aa67d732ac40e4c253df6728543b928a@noreply.gitlab.ci.csc.fi>
Merged by Monika Radaviciute <mradavic@csc.fi>

662 of 938 branches covered (70.58%)

Branch coverage included in aggregate %.

6194 of 10945 relevant lines covered (56.59%)

5.17 hits per line

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

59.19
/src/components/SubmissionWizard/WizardForms/WizardFillObjectDetailsForm.tsx
1
/* Breaking change for JSON schema version draft-2020-12:
1✔
2
 * https://ajv.js.org/json-schema.html#draft-2020-12
3
 */
4
import React, { useEffect, useState, useRef, useTransition, RefObject } from "react"
1✔
5

6
import CancelIcon from "@mui/icons-material/Cancel"
1✔
7
import { GlobalStyles } from "@mui/material"
1✔
8
import Alert from "@mui/material/Alert"
1✔
9
import Button from "@mui/material/Button"
1✔
10
import CircularProgress from "@mui/material/CircularProgress"
1✔
11
import Container from "@mui/material/Container"
1✔
12
import LinearProgress from "@mui/material/LinearProgress"
1✔
13
import Typography from "@mui/material/Typography"
1✔
14
import { Box, styled } from "@mui/system"
1✔
15
import Ajv2020 from "ajv/dist/2020"
1✔
16
import { ApiResponse } from "apisauce"
17
import { cloneDeep, set } from "lodash"
1✔
18
import { useForm, FormProvider, FieldValues, SubmitHandler } from "react-hook-form"
1✔
19
import type { FieldErrors, UseFormReturn } from "react-hook-form"
20
import { useTranslation } from "react-i18next"
1✔
21

22
import WizardStepContentHeader from "../WizardComponents/WizardStepContentHeader"
1✔
23
import getLinkedDereferencedSchema from "../WizardHooks/WizardLinkedDereferencedSchemaHook"
1✔
24
import saveDraftHook from "../WizardHooks/WizardSaveDraftHook"
1✔
25
import submitObjectHook from "../WizardHooks/WizardSubmitObjectHook"
1✔
26

27
import { WizardAjvResolver } from "./WizardAjvResolver"
1✔
28
import JSONSchemaParser from "./WizardJSONSchemaParser"
1✔
29
import WizardOptions from "./WizardOptions"
1✔
30
import WizardXMLUploadModal from "./WizardXMLUploadModal"
1✔
31

32
import { ResponseStatus } from "constants/responseStatus"
1✔
33
import { ObjectStatus, ObjectTypes, ObjectSubmissionTypes } from "constants/wizardObject"
1✔
34
import { resetAutocompleteField } from "features/autocompleteSlice"
1✔
35
import { setClearForm } from "features/clearFormSlice"
1✔
36
import { setDraftStatus, resetDraftStatus } from "features/draftStatusSlice"
1✔
37
import { setFileTypes, deleteFileType } from "features/fileTypesSlice"
1✔
38
import { updateStatus } from "features/statusMessageSlice"
1✔
39
import { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice"
1✔
40
import {
41
  deleteObjectFromSubmission,
42
  replaceObjectInSubmission,
43
  addDoiInfoToSubmission,
44
} from "features/wizardSubmissionSlice"
1✔
45
import { setXMLModalOpen, resetXMLModalOpen } from "features/wizardXMLModalSlice"
1✔
46
import { useAppSelector, useAppDispatch } from "hooks"
1✔
47
import objectAPIService from "services/objectAPI"
1✔
48
import schemaAPIService from "services/schemaAPI"
1✔
49
import type {
50
  DoiFormDetails,
51
  SubmissionDetailsWithId,
52
  FormDataFiles,
53
  FormObject,
54
  ObjectDetails,
55
  ObjectDisplayValues,
56
  HandlerRef,
57
} from "types"
58
import {
59
  getObjectDisplayTitle,
60
  getAccessionIds,
61
  getNewUniqueFileTypes,
62
  checkObjectStatus,
63
} from "utils"
1✔
64
import { dereferenceSchema } from "utils/JSONSchemaUtils"
1✔
65

66
const CustomAlert = styled(Alert)(({ theme, severity }) => ({
1✔
67
  backgroundColor: theme.palette.background.paper,
×
68
  borderLeft: `1.25rem solid ${
×
69
    severity === "error" ? theme.palette.error.main : theme.palette.success.main
×
70
  }`,
71
  borderTop: `0.25rem solid ${
×
72
    severity === "error" ? theme.palette.error.main : theme.palette.success.main
×
73
  }`,
74
  borderRight: `0.25rem solid ${
×
75
    severity === "error" ? theme.palette.error.main : theme.palette.success.main
×
76
  }`,
77
  borderBottom: `0.25rem solid ${
×
78
    severity === "error" ? theme.palette.error.main : theme.palette.success.main
×
79
  }`,
80
  "& .MuiAlert-icon": {
×
81
    display: "flex",
×
82
    alignItems: "center",
×
83
    justifyContent: "center",
×
84
    padding: "0 0.5rem",
×
85
  },
×
86
  lineHeight: "1.75",
×
87
  boxShadow: "0 0.25rem 0.625rem rgba(0, 0, 0, 0.2)",
×
88
  position: "relative",
×
89
  padding: "1rem",
×
90
  display: "flex",
×
91
  justifyContent: "flex-start",
×
92
  alignItems: "center",
×
93
  width: "40%",
×
94
  margin: "6.25rem auto 0 auto",
×
95
}))
1✔
96

97
const AlertMessage = styled(Typography)({
1✔
98
  fontWeight: "bold",
1✔
99
})
1✔
100

101
const ButtonGroup = styled(Box)(({ theme }) => ({
1✔
102
  display: "flex",
16✔
103
  gridTemplateColumns: "repeat(3, 1fr)",
16✔
104
  columnGap: "2rem",
16✔
105
  marginLeft: "2rem",
16✔
106
  marginRight: "3rem",
16✔
107
  "& button": {
16✔
108
    backgroundColor: theme.palette.primary.main,
16✔
109
    color: theme.palette.common.white,
16✔
110
    height: "4.5rem",
16✔
111
    width: "16rem",
16✔
112
  },
16✔
113
}))
1✔
114

115
const Form = styled("form")(({ theme }) => ({
1✔
116
  ...theme.form,
16✔
117
}))
1✔
118

119
type CustomCardHeaderProps = {
120
  hasSubmittedObject: boolean
121
  hasDraftObject: boolean
122
  objectType: string
123
  currentObject: ObjectDetails
124
  onClickSaveDraft: () => Promise<void>
125
  onClickSubmit: () => void
126
  onClickSaveDOI: () => Promise<void>
127
  onClickClearForm: () => void
128
  onOpenXMLModal: () => void
129
  onDeleteForm: () => void
130
  refForm: string
131
}
132

133
type FormContentProps = {
134
  methods: UseFormReturn
135
  formSchema: FormObject
136
  onSubmit: SubmitHandler<FieldValues>
137
  objectType: string
138
  submission: SubmissionDetailsWithId
139
  hasDraftObject: boolean
140
  hasSubmittedObject: boolean
141
  currentObject: ObjectDetails & { objectId: string; [key: string]: unknown }
142
  ref: HandlerRef
143
}
144

145
/*
146
 * Create header for form card with button to close the card
147
 */
148
const CustomCardHeader = (props: CustomCardHeaderProps) => {
1✔
149
  const {
16✔
150
    hasSubmittedObject,
16✔
151
    hasDraftObject,
16✔
152
    objectType,
16✔
153
    currentObject,
16✔
154
    refForm,
16✔
155
    onClickSaveDraft,
16✔
156
    onClickSubmit,
16✔
157
    onClickSaveDOI,
16✔
158
    onClickClearForm,
16✔
159
    onOpenXMLModal,
16✔
160
    onDeleteForm,
16✔
161
  } = props
16✔
162

163
  const focusTarget = useRef<HTMLButtonElement>(null)
16✔
164
  const shouldFocus = useAppSelector(state => state.focus)
16✔
165
  const { t } = useTranslation()
16✔
166

167
  useEffect(() => {
16✔
168
    if (shouldFocus && focusTarget.current) focusTarget.current.focus()
7!
169
  }, [shouldFocus])
16✔
170

171
  const buttonGroup = (
16✔
172
    <Box display="flex">
16✔
173
      <WizardOptions
16✔
174
        objectType={objectType}
16✔
175
        onClearForm={onClickClearForm}
16✔
176
        onOpenXMLModal={onOpenXMLModal}
16✔
177
        onDeleteForm={onDeleteForm}
16✔
178
        disableUploadXML={
16✔
179
          objectType === ObjectTypes.study && (hasDraftObject || hasSubmittedObject)
16✔
180
        }
16✔
181
      />
182
      <ButtonGroup>
16✔
183
        <Button
16✔
184
          variant="contained"
16✔
185
          aria-label="save form as draft"
16✔
186
          size="small"
16✔
187
          onClick={onClickSaveDraft}
16✔
188
          data-testid="form-draft"
16✔
189
          disabled={objectType === ObjectTypes.study && hasSubmittedObject}
16✔
190
        >
191
          {(objectType === ObjectTypes.study && hasDraftObject) ||
16✔
192
          currentObject?.status === ObjectStatus.draft
16!
193
            ? t("formActions.updateDraft")
×
194
            : t("formActions.saveAsDraft")}
16✔
195
        </Button>
16✔
196
        <Button
16✔
197
          variant="contained"
16✔
198
          aria-label="submit form"
16✔
199
          size="small"
16✔
200
          type="submit"
16✔
201
          onClick={onClickSubmit}
16✔
202
          form={refForm}
16✔
203
          data-testid="form-ready"
16✔
204
          disabled={
16✔
205
            objectType === ObjectTypes.study && hasSubmittedObject && !currentObject.accessionId
16✔
206
          }
207
        >
208
          {(objectType === ObjectTypes.study && hasSubmittedObject) ||
16✔
209
          currentObject?.status === ObjectStatus.submitted
5✔
210
            ? t("formActions.update")
11✔
211
            : t("formActions.markAsReady")}
5✔
212
        </Button>
16✔
213
      </ButtonGroup>
16✔
214
    </Box>
16✔
215
  )
216

217
  const doiButtonGroup = (
16✔
218
    <Box display="flex">
16✔
219
      <WizardOptions
16✔
220
        objectType={objectType}
16✔
221
        onClearForm={onClickClearForm}
16✔
222
        onOpenXMLModal={onOpenXMLModal}
16✔
223
        onDeleteForm={onDeleteForm}
16✔
224
      />
225
      <ButtonGroup>
16✔
226
        <Button
16✔
227
          variant="contained"
16✔
228
          aria-label="save Datacite"
16✔
229
          size="small"
16✔
230
          onClick={onClickSaveDOI}
16✔
231
          data-testid="form-datacite"
16✔
232
        >
233
          {t("save")}
16✔
234
        </Button>
16✔
235
      </ButtonGroup>
16✔
236
    </Box>
16✔
237
  )
238

239
  return (
16✔
240
    <WizardStepContentHeader
16✔
241
      action={objectType === ObjectTypes.datacite ? doiButtonGroup : buttonGroup}
16!
242
    />
243
  )
244
}
16✔
245

246
/*
247
 * Draft save and object patch use both same response handler
248
 */
249
const patchHandler = (
1✔
250
  response: ApiResponse<unknown>,
×
251
  submission: SubmissionDetailsWithId,
×
252
  accessionId: string,
×
253
  objectType: string,
×
254
  cleanedValues: Record<string, unknown>,
×
255
  dispatch: (reducer: unknown) => void
×
256
) => {
×
257
  if (response.ok) {
×
258
    dispatch(
×
259
      replaceObjectInSubmission(
×
260
        accessionId,
×
261
        {
×
262
          submissionType: ObjectSubmissionTypes.form,
×
263
          displayTitle: getObjectDisplayTitle(objectType, cleanedValues as ObjectDisplayValues),
×
264
        },
×
265
        ObjectStatus.submitted
×
266
      )
×
267
    )
×
268
    dispatch(resetDraftStatus())
×
269
    dispatch(
×
270
      updateStatus({
×
271
        status: ResponseStatus.success,
×
272
        response: response,
×
273
        helperText: "",
×
274
      })
×
275
    )
×
276
  } else {
×
277
    dispatch(
×
278
      updateStatus({
×
279
        status: ResponseStatus.error,
×
280
        response: response,
×
281
        helperText: "Unexpected error",
×
282
      })
×
283
    )
×
284
  }
×
285
}
×
286

287
/*
288
 * Return react-hook-form based form which is rendered from schema and checked against resolver. Set default values when continuing draft
289
 */
290
const FormContent = ({
1✔
291
  methods,
16✔
292
  formSchema,
16✔
293
  onSubmit,
16✔
294
  objectType,
16✔
295
  hasDraftObject,
16✔
296
  hasSubmittedObject,
16✔
297
  submission,
16✔
298
  currentObject,
16✔
299
  ref,
16✔
300
}: FormContentProps) => {
16✔
301
  const dispatch = useAppDispatch()
16✔
302
  const { t } = useTranslation()
16✔
303

304
  const alert = useAppSelector(state => state.alert)
16✔
305
  const clearForm = useAppSelector(state => state.clearForm)
16✔
306

307
  const [currentObjectId, setCurrentObjectId] = useState<string | null>(currentObject?.accessionId)
16✔
308
  const [draftAutoSaveAllowed, setDraftAutoSaveAllowed] = useState(false)
16✔
309

310
  const autoSaveTimer: { current: NodeJS.Timeout | null } = useRef(null)
16✔
311
  let timer = 0
16✔
312

313
  // Set form default values
314
  useEffect(() => {
16✔
315
    try {
7✔
316
      const mutable = cloneDeep(currentObject)
7✔
317
      methods.reset(mutable)
7✔
318
    } catch (e) {
7!
319
      console.error("Reset failed:", e)
×
320
    }
×
321
  }, [currentObject?.accessionId])
16✔
322

323
  useEffect(() => {
16✔
324
    dispatch(setClearForm(false))
7✔
325
  }, [clearForm])
16✔
326

327
  // Check if form has been edited
328
  useEffect(() => {
16✔
329
    checkDirty()
8✔
330
  }, [methods.formState.isDirty])
16✔
331

332
  const { isSubmitSuccessful } = methods.formState // Check if the form has been successfully submitted without any errors
16✔
333

334
  useEffect(() => {
16✔
335
    // Delete draft form ONLY if the form was successfully submitted
336
    if (isSubmitSuccessful) {
7!
337
      if (
×
338
        currentObject?.status === ObjectStatus.draft &&
×
339
        currentObjectId &&
×
340
        Object.keys(currentObject).length > 0
×
341
      ) {
×
342
        handleDeleteForm()
×
343
      }
×
344
    }
×
345
  }, [isSubmitSuccessful])
16✔
346

347
  const handleClearForm = () => {
16✔
348
    resetTimer()
×
349
    methods.reset({ undefined })
×
350
    dispatch(setClearForm(true))
×
351
    dispatch(
×
352
      setCurrentObject({
×
353
        objectId: currentObjectId,
×
354
        status: currentObject.status,
×
355
      })
×
356
    )
×
357
  }
×
358

359
  // Check if the form is empty
360
  const isFormCleanedValuesEmpty = (cleanedValues: {
16✔
361
    [x: string]: unknown
362
    [x: number]: unknown
363
    accessionId?: string
364
    lastModified?: string
365
    objectType?: string
366
    status?: string
367
    title?: string
368
    submissionType?: string
369
  }) => {
2✔
370
    return Object.keys(cleanedValues).filter(val => val !== "index").length === 0
2✔
371
  }
2✔
372

373
  const checkDirty = () => {
16✔
374
    const isFormTouched = () => {
10✔
375
      return Object.keys(methods.formState.dirtyFields).length > 0
10✔
376
    }
10✔
377

378
    if (isFormTouched()) {
10✔
379
      dispatch(setDraftStatus("notSaved"))
2✔
380
    } else dispatch(resetDraftStatus())
10✔
381
  }
10✔
382

383
  const getCleanedValues = () =>
16✔
384
    JSONSchemaParser.cleanUpFormValues(methods.getValues()) as ObjectDetails
2✔
385

386
  // Draft data is set to state on every change to form
387
  const handleChange = () => {
16✔
388
    clearForm ? dispatch(setClearForm(false)) : null
2!
389
    const clone = cloneDeep(currentObject)
2✔
390
    const values = getCleanedValues()
2✔
391

392
    if (clone && !isFormCleanedValuesEmpty(values)) {
2✔
393
      Object.keys(values).forEach(item => (clone[item] = values[item]))
2✔
394

395
      !currentObject.accessionId && currentObjectId
2!
396
        ? dispatch(
×
397
            setCurrentObject({
×
398
              ...clone,
×
399
              cleanedValues: values,
×
400
              status: currentObject.status || ObjectStatus.draft,
×
401
              objectId: currentObjectId,
×
402
            })
×
403
          )
×
404
        : dispatch(setCurrentObject({ ...clone, cleanedValues: values }))
2✔
405
      checkDirty()
2✔
406
    } else {
2!
407
      dispatch(resetDraftStatus())
×
408
      resetTimer()
×
409
    }
×
410
  }
2✔
411

412
  const handleDOISubmit = async (data: DoiFormDetails) => {
16✔
413
    dispatch(addDoiInfoToSubmission(submission.submissionId, data))
×
414
      .then(() => {
×
415
        dispatch(resetAutocompleteField())
×
416
        dispatch(resetCurrentObject())
×
417
        dispatch(
×
418
          updateStatus({
×
419
            status: ResponseStatus.success,
×
420
            helperText: "snackbarMessages.success.doi.saved",
×
421
          })
×
422
        )
×
423
      })
×
424
      .catch(error =>
×
425
        dispatch(
×
426
          updateStatus({
×
427
            status: ResponseStatus.error,
×
428
            response: error,
×
429
            helperText: "snackbarMessages.error.helperText.submitDoiError",
×
430
          })
×
431
        )
×
432
      )
×
433
  }
×
434

435
  const handleValidationErrors = (errors: FieldErrors) => {
16✔
436
    const missingRequired = Object.values(errors).some(err =>
×
437
      err?.message?.toString().includes("required")
×
438
    )
×
439
    const message = missingRequired
×
440
      ? t("snackbarMessages.info.requiredFields")
×
441
      : t("snackbarMessages.info.invalidFields")
×
442
    dispatch(
×
443
      updateStatus({
×
444
        status: ResponseStatus.info,
×
445
        helperText: message,
×
446
      })
×
447
    )
×
448
  }
×
449

450
  /*
451
   * Logic for auto-save feature.
452
   * We use setDraftAutoSaveAllowed state change to render form before save.
453
   * This helps with getting current accession ID and form data without rendering on every timer increment.
454
   */
455

456
  const startTimer = () => {
16✔
457
    autoSaveTimer.current = setInterval(() => {
2✔
458
      timer = timer + 1
×
459
      if (timer >= 60) {
×
460
        setDraftAutoSaveAllowed(true)
×
461
      }
×
462
    }, 1000)
2✔
463
  }
2✔
464

465
  const resetTimer = () => {
16✔
466
    setDraftAutoSaveAllowed(false)
9✔
467
    clearInterval(autoSaveTimer.current as NodeJS.Timeout)
9✔
468
    timer = 0
9✔
469
  }
9✔
470

471
  useEffect(() => {
16✔
472
    if (alert) resetTimer()
7!
473

474
    if (draftAutoSaveAllowed) {
7!
475
      handleSaveDraft()
×
476
      resetTimer()
×
477
    }
×
478
  }, [draftAutoSaveAllowed, alert])
16✔
479

480
  const keyHandler = () => {
16✔
481
    resetTimer()
2✔
482

483
    // Prevent auto save from DOI form
484
    if (![ObjectTypes.datacite, ObjectTypes.study].includes(objectType)) startTimer()
2✔
485
  }
2✔
486

487
  useEffect(() => {
16✔
488
    window.addEventListener("keydown", keyHandler)
7✔
489
    return () => {
7✔
490
      resetTimer()
7✔
491
      window.removeEventListener("keydown", keyHandler)
7✔
492
    }
7✔
493
  }, [])
16✔
494

495
  const emptyFormError = () => {
16✔
496
    dispatch(
×
497
      updateStatus({
×
498
        status: ResponseStatus.info,
×
499
        helperText: t("snackbarMessages.info.emptyForm"),
×
500
      })
×
501
    )
×
502
  }
×
503

504
  /*
505
   * Update or save new draft depending on object status
506
   */
507
  const handleSaveDraft = async () => {
16✔
508
    resetTimer()
×
509
    const cleanedValues = getCleanedValues()
×
510

511
    if (!isFormCleanedValuesEmpty(cleanedValues)) {
×
512
      const handleSave = await saveDraftHook({
×
513
        accessionId: currentObject.accessionId || currentObject.objectId,
×
514
        objectType: objectType,
×
515
        objectStatus: currentObject.status,
×
516
        submission: submission,
×
517
        values: cleanedValues,
×
518
        dispatch: dispatch,
×
519
      })
×
520

521
      if (handleSave.ok && currentObject?.status !== ObjectStatus.submitted) {
×
522
        setCurrentObjectId(handleSave.data.accessionId)
×
523
        const clone = cloneDeep(currentObject)
×
524
        dispatch(
×
525
          setCurrentObject({
×
526
            ...clone,
×
527
            status: currentObject.status || ObjectStatus.draft,
×
528
            accessionId: handleSave.data.accessionId,
×
529
          })
×
530
        )
×
531
        dispatch(resetDraftStatus())
×
532
      }
×
533
    } else {
×
534
      emptyFormError()
×
535
    }
×
536
  }
×
537

538
  const handleXMLModalOpen = () => {
16✔
539
    dispatch(setXMLModalOpen())
×
540
  }
×
541

542
  const handleDeleteForm = async () => {
16✔
543
    if (currentObjectId) {
×
544
      try {
×
545
        await dispatch(
×
546
          deleteObjectFromSubmission(currentObject.status, currentObjectId, objectType)
×
547
        )
×
548
        handleReset()
×
549
        handleChange()
×
550
        dispatch(resetCurrentObject())
×
551

552
        // Delete fileType that is equivalent to deleted object (for Run and Analysis cases)
553
        if (objectType === ObjectTypes.analysis || objectType === ObjectTypes.run) {
×
554
          dispatch(deleteFileType(currentObjectId))
×
555
        }
×
556
      } catch (error) {
×
557
        dispatch(
×
558
          updateStatus({
×
559
            status: ResponseStatus.error,
×
560
            response: error,
×
561
            helperText: "snackbarMessages.error.helperText.deleteObjectFromSubmission",
×
562
          })
×
563
        )
×
564
      }
×
565
    }
×
566
  }
×
567

568
  const handleReset = () => {
16✔
569
    methods.reset({ undefined })
×
570
    setCurrentObjectId(null)
×
571
  }
×
572

573
  return (
16✔
574
    <FormProvider {...methods}>
16✔
575
      <CustomCardHeader
16✔
576
        hasSubmittedObject={hasSubmittedObject}
16✔
577
        hasDraftObject={hasDraftObject}
16✔
578
        objectType={objectType}
16✔
579
        currentObject={currentObject}
16✔
580
        refForm="hook-form"
16✔
581
        onClickSaveDraft={() => handleSaveDraft()}
16✔
582
        onClickSubmit={() => resetTimer()}
16✔
583
        onClickSaveDOI={methods.handleSubmit(
16✔
584
          async data => handleDOISubmit(data as DoiFormDetails),
16✔
585
          handleValidationErrors
16✔
586
        )}
16✔
587
        onClickClearForm={() => handleClearForm()}
16✔
588
        onOpenXMLModal={() => handleXMLModalOpen()}
16✔
589
        onDeleteForm={() => handleDeleteForm()}
16✔
590
      />
591
      <Form
16✔
592
        id="hook-form"
16✔
593
        onChange={() => handleChange()}
16✔
594
        onSubmit={methods.handleSubmit(onSubmit)}
16✔
595
        ref={ref as RefObject<HTMLFormElement>}
16✔
596
        onReset={handleReset}
16✔
597
      >
598
        <Box>{JSONSchemaParser.buildFields(formSchema)}</Box>
16✔
599
      </Form>
16✔
600
    </FormProvider>
16✔
601
  )
602
}
16✔
603

604
/*
605
 * Container for json schema based form. Handles json schema loading, form rendering, form submitting and error/success alerts.
606
 */
607
const WizardFillObjectDetailsForm = ({ ref }: { ref?: HandlerRef }) => {
1✔
608
  const dispatch = useAppDispatch()
33✔
609

610
  const objectType = useAppSelector(state => state.objectType)
33✔
611
  const submission = useAppSelector(state => state.submission)
33✔
612
  const currentObject = useAppSelector(state => state.currentObject)
33✔
613
  const locale = useAppSelector(state => state.locale)
33✔
614
  const openedXMLModal = useAppSelector(state => state.openedXMLModal)
33✔
615

616
  const { hasDraftObject, hasSubmittedObject } = checkObjectStatus(submission, objectType)
33✔
617

618
  // States that will update in useEffect()
619
  const [states, setStates] = useState({
33✔
620
    error: false,
33✔
621
    helperText: "",
33✔
622
    formSchema: {},
33✔
623
    validationSchema: {} as FormObject,
33✔
624
    isLoading: true,
33✔
625
  })
33✔
626
  const resolver = WizardAjvResolver(states.validationSchema, locale)
33✔
627
  const methods = useForm({ mode: "onBlur", resolver })
33✔
628

629
  const [submitting, startTransition] = useTransition()
33✔
630

631
  /*
632
   * Fetch json schema from either session storage or API, set schema and dereferenced version to component state.
633
   */
634
  useEffect(() => {
33✔
635
    const fetchSchema = async () => {
8✔
636
      const schema: string | null = sessionStorage.getItem(`cached_${objectType}_schema`)
8✔
637
      let parsedSchema: FormObject
8✔
638
      const ajv = new Ajv2020()
8✔
639

640
      if (!schema || !ajv.validateSchema(JSON.parse(schema))) {
8✔
641
        const response = await schemaAPIService.getSchemaByObjectType(objectType)
1!
642
        if (response.ok) {
×
643
          parsedSchema = response.data
×
644
          sessionStorage.setItem(`cached_${objectType}_schema`, JSON.stringify(parsedSchema))
×
645
        } else {
×
646
          setStates({
×
647
            ...states,
×
648
            error: true,
×
649
            helperText: "Unfortunately an error happened while catching form fields",
×
650
            isLoading: false,
×
651
          })
×
652
          return
×
653
        }
×
654
      } else {
8✔
655
        parsedSchema = JSON.parse(schema)
7✔
656
      }
7✔
657

658
      // Dereference Schema and link AccessionIds to equivalent objects
659
      let dereferencedSchema: Promise<FormObject> = await dereferenceSchema(
7✔
660
        parsedSchema as FormObject
7✔
661
      )
7✔
662

663
      dereferencedSchema = getLinkedDereferencedSchema(
7✔
664
        currentObject,
7✔
665
        parsedSchema.title.toLowerCase(),
7✔
666
        dereferencedSchema,
7✔
667
        submission.metadataObjects,
7✔
668
        analysisAccessionIds
7✔
669
      )
7✔
670

671
      // In local state also remove "Datacite" from string coming from schema submission.doiInfo.title
672
      setStates({
7✔
673
        ...states,
7✔
674
        formSchema: {
7✔
675
          ...dereferencedSchema,
7✔
676
          title: parsedSchema.title.toLowerCase().includes(ObjectTypes.datacite)
7!
677
            ? parsedSchema.title.slice(9)
✔
678
            : parsedSchema.title,
7✔
679
        },
8✔
680
        validationSchema: parsedSchema,
8✔
681
        isLoading: false,
8✔
682
      })
8✔
683
    }
8✔
684

685
    // In case of there is object type, and Summary amd Publish do not have schema
686
    if (
8✔
687
      objectType.length &&
8✔
688
      objectType !== "file" &&
8✔
689
      objectType !== "Summary" &&
8✔
690
      objectType !== "Publish"
8✔
691
    )
692
      fetchSchema()
8✔
693

694
    // Reset current object in state on unmount
695
    return () => {
8✔
696
      dispatch(resetDraftStatus())
8✔
697
    }
8✔
698
  }, [objectType])
33✔
699

700
  // All Analysis AccessionIds
701
  const analysisAccessionIds = getAccessionIds(ObjectTypes.analysis, submission.metadataObjects)
33✔
702

703
  useEffect(() => {
33✔
704
    if (ObjectTypes.analysis) {
8✔
705
      if (analysisAccessionIds?.length > 0) {
8!
706
        // Link other Analysis AccessionIds to current Analysis form
707
        setStates(prevState => {
×
708
          return set(
×
709
            prevState,
×
710
            `formSchema.properties.analysisRef.items.properties.accessionId.enum`,
×
711
            analysisAccessionIds.filter(id => id !== currentObject?.accessionId)
×
712
          )
×
713
        })
×
714
      }
×
715
    }
8✔
716
  }, [currentObject?.accessionId, analysisAccessionIds?.length])
33✔
717

718
  /*
719
   * Submit form with cleaned values and check for response errors
720
   */
721
  const onSubmit = (data: Record<string, unknown>) => {
33✔
722
    if (Object.keys(data).length === 0) return
×
723

724
    // Handle submitted object update
725
    const patchObject = async () => {
×
726
      const accessionId = data.accessionId as string
×
727
      const cleanedValues = JSONSchemaParser.cleanUpFormValues(data)
×
728
      try {
×
729
        const response = await objectAPIService.patchFromJSON(
×
730
          objectType,
×
731
          accessionId,
×
732
          cleanedValues
×
733
        )
×
734

735
        patchHandler(
×
736
          response,
×
737
          submission,
×
738
          currentObject.accessionId,
×
739
          objectType,
×
740
          cleanedValues,
×
741
          dispatch
×
742
        )
×
743
        dispatch(resetCurrentObject())
×
744
        methods.reset({ undefined })
×
745
        // Dispatch fileTypes if object is Run or Analysis
746
        if (objectType === ObjectTypes.run || objectType === ObjectTypes.analysis) {
×
747
          const objectWithFileTypes = getNewUniqueFileTypes(
×
748
            accessionId,
×
749
            cleanedValues as FormDataFiles
×
750
          )
×
751
          objectWithFileTypes ? dispatch(setFileTypes(objectWithFileTypes)) : null
×
752
        }
×
753
      } catch (error) {
×
754
        dispatch(
×
755
          updateStatus({
×
756
            status: ResponseStatus.error,
×
757
            response: error,
×
758
            helperText: "Unexpected error when modifying object",
×
759
          })
×
760
        )
×
761
      }
×
762
    }
×
763

764
    startTransition(async () => {
×
765
      // Either patch object or submit a new object
766
      if (data.status === ObjectStatus.submitted) {
×
767
        await patchObject()
×
768
      } else {
×
769
        await submitObjectHook(data, submission.submissionId, objectType, dispatch)
×
770
        methods.reset({ undefined })
×
771
      }
×
772
    })
×
773
  }
×
774

775
  if (states.isLoading) return <CircularProgress />
33✔
776
  // Schema validation error differs from response status handler
777
  if (states.error)
17✔
778
    return (
17!
779
      <CustomAlert severity="error" icon={<CancelIcon sx={{ fontSize: "2rem" }} />}>
×
780
        <AlertMessage>{states.helperText}</AlertMessage>
×
781
      </CustomAlert>
✔
782
    )
783

784
  return (
17✔
785
    <>
17✔
786
      <GlobalStyles styles={{ ".MuiContainer-root": { maxWidth: "100% !important" } }} />
17✔
787
      <Container sx={{ m: 0, p: 0, width: "100%", boxSizing: "border-box" }} maxWidth={false}>
17✔
788
        <FormContent
17✔
789
          formSchema={states.formSchema as FormObject}
17✔
790
          methods={methods}
17✔
791
          onSubmit={onSubmit as SubmitHandler<FieldValues>}
17✔
792
          objectType={objectType}
17✔
793
          hasDraftObject={hasDraftObject}
17✔
794
          hasSubmittedObject={hasSubmittedObject}
17✔
795
          submission={submission}
17✔
796
          currentObject={currentObject}
17✔
797
          key={currentObject?.accessionId || submission.submissionId}
17✔
798
          ref={ref}
17✔
799
        />
800
        {submitting && <LinearProgress />}
33!
801
        <WizardXMLUploadModal
33✔
802
          open={openedXMLModal}
33✔
803
          handleClose={() => {
33✔
804
            dispatch(resetXMLModalOpen())
×
805
          }}
×
806
        />
807
      </Container>
33✔
808
    </>
33✔
809
  )
810
}
33✔
811

812
export default WizardFillObjectDetailsForm
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