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

CSCfi / metadata-submitter-frontend / 16717616310

04 Aug 2025 08:07AM UTC coverage: 57.842% (-0.2%) from 58.001%
16717616310

push

github

Hang Le
Saving and editing Identifier step (merge commit)

Merge branch 'bugfix/edit-doi-after-save' into 'main'
* Fixed DOI fields editing

* Fix dataset title clearing upon save

* Comment out drafts, remove Options

* Fix Describe step clearing

* Fix clearing DOI form

* Fix affiliation and keywords saving and editing

* Multiple keywords saving possible

* Edit and clear Identifier step

Closes #1035 and #1036
See merge request https://gitlab.ci.csc.fi/sds-dev/sd-submit/metadata-submitter-frontend/-/merge_requests/1120

Reviewed-by: Liisa Lado-Villar <145-lilado@users.noreply.gitlab.ci.csc.fi>
Approved-by: Hang Le <lhang@csc.fi>
Co-authored-by: Mariia Rogina <roginama@csc.fi>
Merged by Hang Le <lhang@csc.fi>

641 of 910 branches covered (70.44%)

Branch coverage included in aggregate %.

9 of 16 new or added lines in 2 files covered. (56.25%)

15 existing lines in 2 files now uncovered.

6123 of 10784 relevant lines covered (56.78%)

5.02 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

218
  const doiButtonGroup = (
16✔
219
    <Box display="flex">
16✔
220
      <ButtonGroup>
16✔
221
        <Button
16✔
222
          variant="contained"
16✔
223
          aria-label={t("ariaLabels.saveDOI")}
16✔
224
          size="small"
16✔
225
          onClick={onClickSaveDOI}
16✔
226
          data-testid="form-datacite"
16✔
227
        >
228
          {t("save")}
16✔
229
        </Button>
16✔
230
      </ButtonGroup>
16✔
231
    </Box>
16✔
232
  )
233

234
  return (
16✔
235
    <WizardStepContentHeader
16✔
236
      action={objectType === ObjectTypes.datacite ? doiButtonGroup : buttonGroup}
16!
237
    />
238
  )
239
}
16✔
240

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

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

299
  const clearForm = useAppSelector(state => state.clearForm)
16✔
300
  // const alert = useAppSelector(state => state.alert)
301

302
  const [currentObjectId, setCurrentObjectId] = useState<string | null>(currentObject?.accessionId)
16✔
303
  // const [draftAutoSaveAllowed, setDraftAutoSaveAllowed] = useState(false)
304

305
  // const autoSaveTimer: { current: NodeJS.Timeout | null } = useRef(null)
306
  // let timer = 0
307

308
  // const { isSubmitSuccessful } = methods.formState
309

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

320
  useEffect(() => {
16✔
321
    //   if (isSubmitSuccessful) {
322
    // // Reset only draft status
323
    // dispatch(resetDraftStatus())
324
    dispatch(setClearForm(false))
7✔
325
  }, [clearForm])
16✔
326

327
  // useEffect(() => {
328
  //   checkDirty()
329
  // }, [methods.formState.isDirty])
330

331
  const handleClearForm = () => {
16✔
332
    methods.reset({ undefined })
×
333
    dispatch(setClearForm(true))
×
334
    dispatch(
×
335
      setCurrentObject({
×
336
        objectId: currentObjectId,
×
337
        status: currentObject.status,
×
338
      })
×
339
    )
×
340
  }
×
341

342
  // Check if the form is empty
343
  // const isFormCleanedValuesEmpty = (cleanedValues: {
344
  //   [x: string]: unknown
345
  //   [x: number]: unknown
346
  //   accessionId?: string
347
  //   lastModified?: string
348
  //   objectType?: string
349
  //   status?: string
350
  //   title?: string
351
  //   submissionType?: string
352
  // }) => {
353
  //   return Object.keys(cleanedValues).filter(val => val !== "index").length === 0
354
  // }
355

356
  // const checkDirty = () => {
357
  //   const isFormTouched = () => {
358
  //     return Object.keys(methods.formState.dirtyFields).length > 0
359
  //   }
360
  //   // if (isFormTouched()) {
361
  //   //   dispatch(setDraftStatus("notSaved"))
362
  //   // } else dispatch(resetDraftStatus())
363
  // }
364

365
  const getCleanedValues = () =>
16✔
366
    JSONSchemaParser.cleanUpFormValues(methods.getValues()) as ObjectDetails
2✔
367

368
  const handleChange = () => {
16✔
369
    clearForm ? dispatch(setClearForm(false)) : null
2!
370
    const clone = cloneDeep(currentObject)
2✔
371
    const values = getCleanedValues()
2✔
372

373
    if (clone && Object.keys(values).filter(val => val !== "index").length > 0) {
2✔
374
      Object.keys(values).forEach(item => (clone[item] = values[item]))
2✔
375

376
      !currentObject.accessionId && currentObjectId
2!
377
        ? dispatch(
×
378
            setCurrentObject({
×
379
              ...clone,
×
380
              cleanedValues: values,
×
381
              // status: currentObject.status || ObjectStatus.draft,
NEW
382
              status: currentObject.status,
×
383
              objectId: currentObjectId,
×
384
            })
×
385
          )
×
386
        : dispatch(setCurrentObject({ ...clone, cleanedValues: values }))
2✔
387
    }
2✔
388
  }
2✔
389

390
  const handleDOISubmit = async (data: DoiFormDetails) => {
16✔
391
    dispatch(addDoiInfoToSubmission(submission.submissionId, data))
×
392
      .then(() => {
×
393
        dispatch(resetAutocompleteField())
×
394
        // dispatch(resetCurrentObject())
395
        dispatch(
×
396
          updateStatus({
×
397
            status: ResponseStatus.success,
×
398
            helperText: "snackbarMessages.success.doi.saved",
×
399
          })
×
400
        )
×
401
      })
×
402
      .catch(error =>
×
403
        dispatch(
×
404
          updateStatus({
×
405
            status: ResponseStatus.error,
×
406
            response: error,
×
407
            helperText: "snackbarMessages.error.helperText.submitDoi",
×
408
          })
×
409
        )
×
410
      )
×
411
  }
×
412

413
  const handleValidationErrors = (errors: FieldErrors) => {
16✔
414
    const missingRequired = Object.values(errors).some(err =>
×
415
      err?.message?.toString().includes("required")
×
416
    )
×
417
    const message = missingRequired
×
418
      ? t("snackbarMessages.info.requiredFields")
×
419
      : t("snackbarMessages.info.invalidFields")
×
420
    dispatch(
×
421
      updateStatus({
×
422
        status: ResponseStatus.info,
×
423
        helperText: message,
×
424
      })
×
425
    )
×
426
  }
×
427

428
  /*
429
   * Logic for auto-save feature.
430
   * We use setDraftAutoSaveAllowed state change to render form before save.
431
   * This helps with getting current accession ID and form data without rendering on every timer increment.
432
   */
433

434
  // const startTimer = () => {
435
  //   autoSaveTimer.current = setInterval(() => {
436
  //     timer = timer + 1
437
  //     if (timer >= 60) {
438
  //       setDraftAutoSaveAllowed(true)
439
  //     }
440
  //   }, 1000)
441
  // }
442

443
  // const resetTimer = () => {
444
  //   setDraftAutoSaveAllowed(false)
445
  //   clearInterval(autoSaveTimer.current as NodeJS.Timeout)
446
  //   timer = 0
447
  // }
448

449
  // useEffect(() => {
450
  //   if (alert) resetTimer()
451

452
  //   if (draftAutoSaveAllowed) {
453
  //     handleSaveDraft()
454
  //     resetTimer()
455
  //   }
456
  // }, [draftAutoSaveAllowed, alert])
457

458
  // const keyHandler = () => {
459
  //   resetTimer()
460

461
  //   // Prevent auto save from DOI form
462
  //   if (![ObjectTypes.datacite, ObjectTypes.study].includes(objectType)) startTimer()
463
  // }
464

465
  // useEffect(() => {
466
  //   window.addEventListener("keydown", keyHandler)
467
  //   return () => {
468
  //     resetTimer()
469
  //     window.removeEventListener("keydown", keyHandler)
470
  //   }
471
  // }, [])
472

473
  // const emptyFormError = () => {
474
  //   dispatch(
475
  //     updateStatus({
476
  //       status: ResponseStatus.info,
477
  //       helperText: t("snackbarMessages.info.emptyForm"),
478
  //     })
479
  //   )
480
  // }
481

482
  /*
483
   * Update or save new draft depending on object status
484
   */
485
  // const handleSaveDraft = async () => {
486
  //   resetTimer()
487
  //   const cleanedValues = getCleanedValues()
488

489
  //   if (!isFormCleanedValuesEmpty(cleanedValues)) {
490
  //     const handleSave = await saveDraftHook({
491
  //       accessionId: currentObject.accessionId || currentObject.objectId,
492
  //       objectType: objectType,
493
  //       objectStatus: currentObject.status,
494
  //       submission: submission,
495
  //       values: cleanedValues,
496
  //       dispatch: dispatch,
497
  //     })
498

499
  //     if (handleSave.ok && currentObject?.status !== ObjectStatus.submitted) {
500
  //       setCurrentObjectId(handleSave.data.accessionId)
501
  //       const clone = cloneDeep(currentObject)
502
  //       dispatch(
503
  //         setCurrentObject({
504
  //           ...clone,
505
  //           status: currentObject.status || ObjectStatus.draft,
506
  //           accessionId: handleSave.data.accessionId,
507
  //         })
508
  //       )
509
  //       dispatch(resetDraftStatus())
510
  //       dispatch(resetCurrentObject())
511
  //       setTimeout(() => methods.reset({}), 0)
512
  //     }
513
  //   } else {
514
  //     emptyFormError()
515
  //   }
516
  // }
517

518
  /*const handleXMLModalOpen = () => {
519
    dispatch(setXMLModalOpen())
520
  }*/
521

522
  const handleDeleteForm = async () => {
16✔
523
    if (currentObjectId) {
×
524
      try {
×
525
        await dispatch(
×
526
          deleteObjectFromSubmission(currentObject.status, currentObjectId, objectType)
×
527
        )
×
528
        handleReset()
×
529
        handleChange()
×
530
        dispatch(resetCurrentObject())
×
531

UNCOV
532
        if (objectType === ObjectTypes.analysis || objectType === ObjectTypes.run) {
×
533
          dispatch(deleteFileType(currentObjectId))
×
534
        }
×
535
      } catch (error) {
×
536
        dispatch(
×
537
          updateStatus({
×
538
            status: ResponseStatus.error,
×
539
            response: error,
×
540
            helperText: "snackbarMessages.error.helperText.deleteObject",
×
541
          })
×
542
        )
×
543
      }
×
544
    }
×
545
  }
×
546

547
  const handleReset = () => {
16✔
548
    methods.reset({ undefined })
×
549
    setCurrentObjectId(null)
×
550
  }
×
551

552
  return (
16✔
553
    <FormProvider {...methods}>
16✔
554
      <CustomCardHeader
16✔
555
        hasSubmittedObject={hasSubmittedObject}
16✔
556
        // hasDraftObject={false}
557
        objectType={objectType}
16✔
558
        currentObject={currentObject}
16✔
559
        refForm="hook-form"
16✔
560
        onClickSubmit={() => {}}
16✔
561
        onClickSaveDOI={methods.handleSubmit(
16✔
562
          async data => handleDOISubmit(data as DoiFormDetails),
16✔
563
          handleValidationErrors
16✔
564
        )}
16✔
565
        onClickClearForm={() => handleClearForm()}
16✔
566
        //onOpenXMLModal={() => handleXMLModalOpen()}
567
        onDeleteForm={() => handleDeleteForm()}
16✔
568
      />
569
      <Form
16✔
570
        id="hook-form"
16✔
571
        onChange={() => handleChange()}
16✔
572
        onSubmit={methods.handleSubmit(onSubmit)}
16✔
573
        ref={ref as RefObject<HTMLFormElement>}
16✔
574
        onReset={handleReset}
16✔
575
      >
576
        <Box>{JSONSchemaParser.buildFields(formSchema)}</Box>
16✔
577
      </Form>
16✔
578
    </FormProvider>
16✔
579
  )
580
}
16✔
581

582
/*
583
 * Container for json schema based form. Handles json schema loading, form rendering, form submitting and error/success alerts.
584
 */
585
const WizardFillObjectDetailsForm = ({ ref }: { ref?: HandlerRef }) => {
1✔
586
  const dispatch = useAppDispatch()
32✔
587

588
  const objectType = useAppSelector(state => state.objectType)
32✔
589
  const submission = useAppSelector(state => state.submission)
32✔
590
  const currentObject = useAppSelector(state => state.currentObject)
32✔
591
  const locale = useAppSelector(state => state.locale)
32✔
592
  //const openedXMLModal = useAppSelector(state => state.openedXMLModal)
593

594
  const { t } = useTranslation()
32✔
595
  const { hasSubmittedObject } = checkObjectStatus(submission, objectType)
32✔
596

597
  // States that will update in useEffect()
598
  const [states, setStates] = useState({
32✔
599
    error: false,
32✔
600
    helperText: "",
32✔
601
    formSchema: {},
32✔
602
    validationSchema: {} as FormObject,
32✔
603
    isLoading: true,
32✔
604
  })
32✔
605
  const resolver = WizardAjvResolver(states.validationSchema, locale)
32✔
606
  const methods = useForm({ mode: "onBlur", resolver })
32✔
607

608
  const [submitting, startTransition] = useTransition()
32✔
609

610
  /*
611
   * Fetch json schema from either session storage or API, set schema and dereferenced version to component state.
612
   */
613
  useEffect(() => {
32✔
614
    const fetchSchema = async () => {
8✔
615
      const schema: string | null = sessionStorage.getItem(`cached_${objectType}_schema`)
8✔
616
      let parsedSchema: FormObject
8✔
617
      const ajv = new Ajv2020()
8✔
618

619
      if (!schema || !ajv.validateSchema(JSON.parse(schema))) {
8✔
620
        const response = await schemaAPIService.getSchemaByObjectType(objectType)
1!
UNCOV
621
        if (response.ok) {
×
622
          parsedSchema = response.data
×
623
          sessionStorage.setItem(`cached_${objectType}_schema`, JSON.stringify(parsedSchema))
×
UNCOV
624
        } else {
×
UNCOV
625
          setStates({
×
UNCOV
626
            ...states,
×
UNCOV
627
            error: true,
×
UNCOV
628
            helperText: t("snackbarMessages.error.helperText.cacheFormFields"),
×
UNCOV
629
            isLoading: false,
×
UNCOV
630
          })
×
UNCOV
631
          return
×
UNCOV
632
        }
×
633
      } else {
8✔
634
        parsedSchema = JSON.parse(schema)
7✔
635
      }
7✔
636

637
      // Dereference Schema and link AccessionIds to equivalent objects
638
      let dereferencedSchema: Promise<FormObject> = await dereferenceSchema(
7✔
639
        parsedSchema as FormObject
7✔
640
      )
7✔
641

642
      dereferencedSchema = getLinkedDereferencedSchema(
7✔
643
        currentObject,
7✔
644
        parsedSchema.title.toLowerCase(),
7✔
645
        dereferencedSchema,
7✔
646
        submission.metadataObjects,
7✔
647
        analysisAccessionIds
7✔
648
      )
7✔
649

650
      // In local state also remove "Datacite" from string coming from schema submission.doiInfo.title
651
      setStates({
7✔
652
        ...states,
7✔
653
        formSchema: {
7✔
654
          ...dereferencedSchema,
7✔
655
          title: parsedSchema.title.toLowerCase().includes(ObjectTypes.datacite)
7!
656
            ? parsedSchema.title.slice(9)
✔
657
            : parsedSchema.title,
7✔
658
        },
8✔
659
        validationSchema: parsedSchema,
8✔
660
        isLoading: false,
8✔
661
      })
8✔
662
    }
8✔
663

664
    // In case of there is object type, and Summary amd Publish do not have schema
665
    if (
8✔
666
      objectType.length &&
8✔
667
      objectType !== "file" &&
8✔
668
      objectType !== "Summary" &&
8✔
669
      objectType !== "Publish"
8✔
670
    )
671
      fetchSchema()
8✔
672
  }, [objectType])
32✔
673

674
  // All Analysis AccessionIds
675
  const analysisAccessionIds = getAccessionIds(ObjectTypes.analysis, submission.metadataObjects)
32✔
676

677
  useEffect(() => {
32✔
678
    if (ObjectTypes.analysis) {
8✔
679
      if (analysisAccessionIds?.length > 0) {
8!
680
        // Link other Analysis AccessionIds to current Analysis form
681
        setStates(prevState => {
×
682
          return set(
×
683
            prevState,
×
684
            `formSchema.properties.analysisRef.items.properties.accessionId.enum`,
×
685
            analysisAccessionIds.filter(id => id !== currentObject?.accessionId)
×
686
          )
×
687
        })
×
688
      }
×
689
    }
8✔
690
  }, [currentObject?.accessionId, analysisAccessionIds?.length])
32✔
691

692
  /*
693
   * Submit form with cleaned values and check for response errors
694
   */
695
  const onSubmit = (data: Record<string, unknown>) => {
32✔
696
    if (Object.keys(data).length === 0) return
×
697

UNCOV
698
    const patchObject = async () => {
×
699
      const accessionId = data.accessionId as string
×
700
      const cleanedValues = JSONSchemaParser.cleanUpFormValues(data)
×
701
      try {
×
702
        const response = await objectAPIService.patchFromJSON(
×
703
          objectType,
×
704
          accessionId,
×
705
          cleanedValues
×
706
        )
×
707

708
        patchHandler(
×
709
          response,
×
710
          submission,
×
711
          currentObject.accessionId,
×
712
          objectType,
×
713
          cleanedValues,
×
714
          dispatch
×
715
        )
×
716

NEW
717
        if (objectType !== ObjectTypes.dataset) {
×
NEW
718
          dispatch(resetCurrentObject())
×
NEW
719
        }
×
720

721
        if (objectType === ObjectTypes.run || objectType === ObjectTypes.analysis) {
×
722
          const objectWithFileTypes = getNewUniqueFileTypes(
×
723
            accessionId,
×
724
            cleanedValues as FormDataFiles
×
725
          )
×
726
          objectWithFileTypes ? dispatch(setFileTypes(objectWithFileTypes)) : null
×
727
        }
×
728
      } catch (error) {
×
729
        dispatch(
×
730
          updateStatus({
×
731
            status: ResponseStatus.error,
×
732
            response: error,
×
733
            helperText: "snackbarMessages.error.helperText.modifyObject",
×
734
          })
×
735
        )
×
736
      }
×
737
    }
×
738

739
    startTransition(async () => {
×
UNCOV
740
      if (data.status === ObjectStatus.submitted) {
×
741
        await patchObject()
×
742
      } else {
×
743
        await submitObjectHook(data, submission.submissionId, objectType, dispatch)
×
744
        // methods.reset({ undefined })
745
      }
×
746
    })
×
747
  }
×
748

749
  if (states.isLoading) return <CircularProgress />
32✔
750
  // Schema validation error differs from response status handler
751
  if (states.error)
16✔
752
    return (
16!
753
      <CustomAlert severity="error" icon={<CancelIcon sx={{ fontSize: "2rem" }} />}>
×
754
        <AlertMessage>{states.helperText}</AlertMessage>
×
755
      </CustomAlert>
✔
756
    )
757

758
  return (
16✔
759
    <>
16✔
760
      <GlobalStyles styles={{ ".MuiContainer-root": { maxWidth: "100% !important" } }} />
16✔
761
      <Container sx={{ m: 0, p: 0, width: "100%", boxSizing: "border-box" }} maxWidth={false}>
16✔
762
        <FormContent
16✔
763
          formSchema={states.formSchema as FormObject}
16✔
764
          methods={methods}
16✔
765
          onSubmit={onSubmit as SubmitHandler<FieldValues>}
16✔
766
          objectType={objectType}
16✔
767
          // hasDraftObject={false}
768
          hasSubmittedObject={hasSubmittedObject}
16✔
769
          submission={submission}
16✔
770
          currentObject={currentObject}
16✔
771
          key={currentObject?.accessionId || submission.submissionId}
16✔
772
          ref={ref}
16✔
773
        />
774
        {submitting && <LinearProgress />}
32!
775
        {/*<WizardXMLUploadModal
776
          open={openedXMLModal}
777
          handleClose={() => {
778
            dispatch(resetXMLModalOpen())
779
          }}
780
        />*/}
781
      </Container>
32✔
782
    </>
32✔
783
  )
784
}
32✔
785

786
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