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

CSCfi / metadata-submitter-frontend / 13652497110

04 Mar 2025 11:17AM UTC coverage: 48.473% (-0.06%) from 48.534%
13652497110

push

github

Hang Le
Update non major dependency pagkages (merge commit)

Merge branch 'update/non-major-dependencies' into 'main'
* update non major dependency pagkages

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

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

654 of 954 branches covered (68.55%)

Branch coverage included in aggregate %.

6109 of 12998 relevant lines covered (47.0%)

3.83 hits per line

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

56.88
/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, 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 { 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 { updateTemplateDisplayTitle } from "features/templateSlice"
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"
1✔
47
import { useAppSelector, useAppDispatch } from "hooks"
1✔
48
import objectAPIService from "services/objectAPI"
1✔
49
import schemaAPIService from "services/schemaAPI"
1✔
50
import templateAPI from "services/templateAPI"
1✔
51
import type {
52
  DoiFormDetails,
53
  SubmissionDetailsWithId,
54
  FormDataFiles,
55
  FormObject,
56
  ObjectDetails,
57
  ObjectDisplayValues,
58
  FormRef,
59
} from "types"
60
import {
61
  getObjectDisplayTitle,
62
  getAccessionIds,
63
  getNewUniqueFileTypes,
64
  checkObjectStatus,
65
} from "utils"
1✔
66
import { dereferenceSchema } from "utils/JSONSchemaUtils"
1✔
67

68
const StickyContainer = styled(Container)(({ theme }) => ({
1✔
69
  position: "sticky",
14✔
70
  top: "0px",
14✔
71
  zIndex: 1200,
14✔
72
  backgroundColor: "white",
14✔
73
  boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
14✔
74
  width: "100%",
14✔
75
  margin: 0,
14✔
76
  paddingLeft: theme.spacing(3),
14✔
77
  paddingRight: theme.spacing(3),
14✔
78
  boxSizing: "border-box",
14✔
79
}))
1✔
80

81
const CustomAlert = styled(Alert)(({ theme, severity }) => ({
1✔
82
  backgroundColor: theme.palette.background.paper,
×
83
  borderLeft: `1.25rem solid ${
×
84
    severity === "error"
×
85
      ? theme.palette.error.main
×
86
      : severity === "info"
×
87
      ? theme.palette.error.light
×
88
      : theme.palette.success.light
×
89
  }`,
90
  borderTop: `0.25rem solid ${
×
91
    severity === "error"
×
92
      ? theme.palette.error.main
×
93
      : severity === "info"
×
94
      ? theme.palette.error.light
×
95
      : theme.palette.success.light
×
96
  }`,
97
  borderRight: `0.25rem solid ${
×
98
    severity === "error"
×
99
      ? theme.palette.error.main
×
100
      : severity === "info"
×
101
      ? theme.palette.error.light
×
102
      : theme.palette.success.light
×
103
  }`,
104
  borderBottom: `0.25rem solid ${
×
105
    severity === "error"
×
106
      ? theme.palette.error.main
×
107
      : severity === "info"
×
108
      ? theme.palette.error.light
×
109
      : theme.palette.success.light
×
110
  }`,
111
  "& .MuiAlert-icon": {
×
112
    display: "flex",
×
113
    alignItems: "center",
×
114
    justifyContent: "center",
×
115
    padding: "0 0.5rem",
×
116
  },
×
117
  color: theme.palette.secondary.main,
×
118
  lineHeight: "1.75",
×
119
  boxShadow: "0 0.25rem 0.625rem rgba(0, 0, 0, 0.2)",
×
120
  position: "relative",
×
121
  padding: "1rem",
×
122
  display: "flex",
×
123
  justifyContent: "flex-start",
×
124
  alignItems: "center",
×
125
  width: "40%",
×
126
  margin: "6.25rem auto 0 auto",
×
127
}))
1✔
128

129
const AlertMessage = styled(Typography)({
1✔
130
  fontWeight: "bold",
1✔
131
})
1✔
132

133
const ButtonGroup = styled(Box)(({ theme }) => ({
1✔
134
  display: "flex",
14✔
135
  gridTemplateColumns: "repeat(3, 1fr)",
14✔
136
  columnGap: "2rem",
14✔
137
  marginLeft: "2rem",
14✔
138
  marginRight: "3rem",
14✔
139
  "& button": {
14✔
140
    backgroundColor: theme.palette.primary.main,
14✔
141
    color: theme.palette.common.white,
14✔
142
    height: "4.5rem",
14✔
143
    width: "16rem",
14✔
144
  },
14✔
145
}))
1✔
146

147
const Form = styled("form")(({ theme }) => ({
1✔
148
  ...theme.form,
14✔
149
}))
1✔
150

151
type CustomCardHeaderProps = {
152
  hasSubmittedObject: boolean
153
  hasDraftObject: boolean
154
  objectType: string
155
  currentObject: ObjectDetails
156
  onClickSaveDraft: () => Promise<void>
157
  onClickUpdateTemplate: () => Promise<void>
158
  onClickSubmit: () => void
159
  onClickSaveDOI: () => Promise<void>
160
  onClickCloseDialog: () => void
161
  onClickClearForm: () => void
162
  onOpenXMLModal: () => void
163
  onDeleteForm: () => void
164
  refForm: string
165
}
166

167
type FormContentProps = {
168
  methods: UseFormReturn
169
  formSchema: FormObject
170
  onSubmit: SubmitHandler<FieldValues>
171
  objectType: string
172
  submission: SubmissionDetailsWithId
173
  hasDraftObject: boolean
174
  hasSubmittedObject: boolean
175
  currentObject: ObjectDetails & { objectId: string; [key: string]: unknown }
176
  closeDialog: () => void
177
  formRef?: FormRef
178
}
179

180
/*
181
 * Create header for form card with button to close the card
182
 */
183
const CustomCardHeader = (props: CustomCardHeaderProps) => {
1✔
184
  const {
14✔
185
    hasSubmittedObject,
14✔
186
    hasDraftObject,
14✔
187
    objectType,
14✔
188
    currentObject,
14✔
189
    refForm,
14✔
190
    onClickSaveDraft,
14✔
191
    onClickUpdateTemplate,
14✔
192
    onClickSubmit,
14✔
193
    onClickSaveDOI,
14✔
194
    onClickCloseDialog,
14✔
195
    onClickClearForm,
14✔
196
    onOpenXMLModal,
14✔
197
    onDeleteForm,
14✔
198
  } = props
14✔
199

200
  const focusTarget = useRef<HTMLButtonElement>(null)
14✔
201
  const shouldFocus = useAppSelector(state => state.focus)
14✔
202
  const { t } = useTranslation()
14✔
203

204
  useEffect(() => {
14✔
205
    if (shouldFocus && focusTarget.current) focusTarget.current.focus()
7!
206
  }, [shouldFocus])
14✔
207

208
  const templateButtonGroup = (
14✔
209
    <ButtonGroup>
14✔
210
      <Button
14✔
211
        type="submit"
14✔
212
        variant="contained"
14✔
213
        aria-label="save form as draft"
14✔
214
        size="small"
14✔
215
        onClick={onClickUpdateTemplate}
14✔
216
      >
217
        Update template
218
      </Button>
14✔
219
      <Button variant="contained" aria-label="clear form" size="small" onClick={onClickCloseDialog}>
14✔
220
        Close
221
      </Button>
14✔
222
    </ButtonGroup>
14✔
223
  )
224

225
  const buttonGroup = (
14✔
226
    <Box display="flex">
14✔
227
      <WizardOptions
14✔
228
        objectType={objectType}
14✔
229
        onClearForm={onClickClearForm}
14✔
230
        onOpenXMLModal={onOpenXMLModal}
14✔
231
        onDeleteForm={onDeleteForm}
14✔
232
        disableUploadXML={
14✔
233
          objectType === ObjectTypes.study && (hasDraftObject || hasSubmittedObject)
14✔
234
        }
14✔
235
      />
236
      <ButtonGroup>
14✔
237
        <Button
14✔
238
          variant="contained"
14✔
239
          aria-label="save form as draft"
14✔
240
          size="small"
14✔
241
          onClick={onClickSaveDraft}
14✔
242
          data-testid="form-draft"
14✔
243
          disabled={objectType === ObjectTypes.study && hasSubmittedObject}
14✔
244
        >
245
          {(objectType === ObjectTypes.study && hasDraftObject) ||
14✔
246
          currentObject?.status === ObjectStatus.draft
14!
247
            ? t("formActions.updateDraft")
×
248
            : t("formActions.saveAsDraft")}
14✔
249
        </Button>
14✔
250
        <Button
14✔
251
          variant="contained"
14✔
252
          aria-label="submit form"
14✔
253
          size="small"
14✔
254
          type="submit"
14✔
255
          onClick={onClickSubmit}
14✔
256
          form={refForm}
14✔
257
          data-testid="form-ready"
14✔
258
          disabled={
14✔
259
            objectType === ObjectTypes.study && hasSubmittedObject && !currentObject.accessionId
14✔
260
          }
261
        >
262
          {(objectType === ObjectTypes.study && hasSubmittedObject) ||
14✔
263
          currentObject?.status === ObjectStatus.submitted
4✔
264
            ? t("formActions.update")
10✔
265
            : t("formActions.markAsReady")}
4✔
266
        </Button>
14✔
267
      </ButtonGroup>
14✔
268
    </Box>
14✔
269
  )
270

271
  const doiButtonGroup = (
14✔
272
    <Box display="flex">
14✔
273
      <WizardOptions
14✔
274
        objectType={objectType}
14✔
275
        onClearForm={onClickClearForm}
14✔
276
        onOpenXMLModal={onOpenXMLModal}
14✔
277
        onDeleteForm={onDeleteForm}
14✔
278
      />
279
      <ButtonGroup>
14✔
280
        <Button
14✔
281
          variant="contained"
14✔
282
          aria-label="save Datacite"
14✔
283
          size="small"
14✔
284
          onClick={onClickSaveDOI}
14✔
285
          data-testid="form-datacite"
14✔
286
        >
287
          {t("save")}
14✔
288
        </Button>
14✔
289
      </ButtonGroup>
14✔
290
    </Box>
14✔
291
  )
292

293
  return (
14✔
294
    <StickyContainer>
14✔
295
      <WizardStepContentHeader
14✔
296
        action={
14✔
297
          currentObject?.status === ObjectStatus.template
14!
298
            ? templateButtonGroup
×
299
            : objectType === "datacite"
14!
300
            ? doiButtonGroup
×
301
            : buttonGroup
14✔
302
        }
14✔
303
      />
304
    </StickyContainer>
14✔
305
  )
306
}
14✔
307

308
/*
309
 * Draft save and object patch use both same response handler
310
 */
311
const patchHandler = (
1✔
312
  response: ApiResponse<unknown>,
×
313
  submission: SubmissionDetailsWithId,
×
314
  accessionId: string,
×
315
  objectType: string,
×
316
  cleanedValues: Record<string, unknown>,
×
317
  dispatch: (reducer: unknown) => void
×
318
) => {
×
319
  if (response.ok) {
×
320
    dispatch(
×
321
      replaceObjectInSubmission(
×
322
        accessionId,
×
323
        {
×
324
          submissionType: ObjectSubmissionTypes.form,
×
325
          displayTitle: getObjectDisplayTitle(objectType, cleanedValues as ObjectDisplayValues),
×
326
        },
×
327
        ObjectStatus.submitted
×
328
      )
×
329
    )
×
330
    dispatch(resetDraftStatus())
×
331
    dispatch(
×
332
      updateStatus({
×
333
        status: ResponseStatus.success,
×
334
        response: response,
×
335
        helperText: "",
×
336
      })
×
337
    )
×
338
  } else {
×
339
    dispatch(
×
340
      updateStatus({
×
341
        status: ResponseStatus.error,
×
342
        response: response,
×
343
        helperText: "Unexpected error",
×
344
      })
×
345
    )
×
346
  }
×
347
}
×
348

349
/*
350
 * Return react-hook-form based form which is rendered from schema and checked against resolver. Set default values when continuing draft
351
 */
352
const FormContent = ({
1✔
353
  methods,
14✔
354
  formSchema,
14✔
355
  onSubmit,
14✔
356
  objectType,
14✔
357
  hasDraftObject,
14✔
358
  hasSubmittedObject,
14✔
359
  submission,
14✔
360
  currentObject,
14✔
361
  closeDialog,
14✔
362
  formRef,
14✔
363
}: FormContentProps) => {
14✔
364
  const dispatch = useAppDispatch()
14✔
365

366
  const alert = useAppSelector(state => state.alert)
14✔
367
  const clearForm = useAppSelector(state => state.clearForm)
14✔
368

369
  const templates = useAppSelector(state => state.templates)
14✔
370

371
  const [currentObjectId, setCurrentObjectId] = useState<string | null>(currentObject?.accessionId)
14✔
372
  const [draftAutoSaveAllowed, setDraftAutoSaveAllowed] = useState(false)
14✔
373

374
  const autoSaveTimer: { current: NodeJS.Timeout | null } = useRef(null)
14✔
375
  let timer = 0
14✔
376

377
  // Set form default values
378
  useEffect(() => {
14✔
379
    methods.reset(currentObject)
7✔
380
  }, [currentObject?.accessionId])
14✔
381

382
  useEffect(() => {
14✔
383
    dispatch(setClearForm(false))
7✔
384
  }, [clearForm])
14✔
385

386
  // Check if form has been edited
387
  useEffect(() => {
14✔
388
    checkDirty()
8✔
389
  }, [methods.formState.isDirty])
14✔
390

391
  const { isSubmitSuccessful } = methods.formState // Check if the form has been successfully submitted without any errors
14✔
392

393
  useEffect(() => {
14✔
394
    // Delete draft form ONLY if the form was successfully submitted
395
    if (isSubmitSuccessful) {
7!
396
      if (
×
397
        currentObject?.status === ObjectStatus.draft &&
×
398
        currentObjectId &&
×
399
        Object.keys(currentObject).length > 0
×
400
      ) {
×
401
        handleDeleteForm()
×
402
      }
×
403
    }
×
404
  }, [isSubmitSuccessful])
14✔
405

406
  const handleClearForm = () => {
14✔
407
    resetTimer()
×
408
    methods.reset({ undefined })
×
409
    dispatch(setClearForm(true))
×
410
    dispatch(
×
411
      setCurrentObject({
×
412
        objectId: currentObjectId,
×
413
        status: currentObject.status,
×
414
      })
×
415
    )
×
416
  }
×
417

418
  // Check if the form is empty
419
  const isFormCleanedValuesEmpty = (cleanedValues: {
14✔
420
    [x: string]: unknown
421
    [x: number]: unknown
422
    accessionId?: string
423
    lastModified?: string
424
    objectType?: string
425
    status?: string
426
    title?: string
427
    submissionType?: string
428
  }) => {
2✔
429
    return Object.keys(cleanedValues).filter(val => val !== "index").length === 0
2✔
430
  }
2✔
431

432
  const checkDirty = () => {
14✔
433
    const isFormTouched = () => {
10✔
434
      return Object.keys(methods.formState.dirtyFields).length > 0
10✔
435
    }
10✔
436

437
    if (isFormTouched()) {
10✔
438
      dispatch(setDraftStatus("notSaved"))
2✔
439
    } else dispatch(resetDraftStatus())
10✔
440
  }
10✔
441

442
  const getCleanedValues = () =>
14✔
443
    JSONSchemaParser.cleanUpFormValues(methods.getValues()) as ObjectDetails
2✔
444

445
  // Draft data is set to state on every change to form
446
  const handleChange = () => {
14✔
447
    clearForm ? dispatch(setClearForm(false)) : null
2!
448
    const clone = cloneDeep(currentObject)
2✔
449
    const values = getCleanedValues()
2✔
450

451
    if (clone && !isFormCleanedValuesEmpty(values)) {
2✔
452
      Object.keys(values).forEach(item => (clone[item] = values[item]))
2✔
453

454
      !currentObject.accessionId && currentObjectId
2!
455
        ? dispatch(
×
456
            setCurrentObject({
×
457
              ...clone,
×
458
              cleanedValues: values,
×
459
              status: currentObject.status || ObjectStatus.draft,
×
460
              objectId: currentObjectId,
×
461
            })
×
462
          )
×
463
        : dispatch(setCurrentObject({ ...clone, cleanedValues: values }))
2✔
464
      checkDirty()
2✔
465
    } else {
2!
466
      dispatch(resetDraftStatus())
×
467
      resetTimer()
×
468
    }
×
469
  }
2✔
470

471
  const handleDOISubmit = async (data: DoiFormDetails) => {
14✔
472
    dispatch(addDoiInfoToSubmission(submission.submissionId, data))
×
473
      .then(() => {
×
474
        dispatch(resetAutocompleteField())
×
475
        dispatch(resetCurrentObject())
×
476
        dispatch(
×
477
          updateStatus({
×
478
            status: ResponseStatus.success,
×
479
            helperText: "snackbarMessages.success.doi.saved",
×
480
          })
×
481
        )
×
482
      })
×
483
      .catch(error =>
×
484
        dispatch(
×
485
          updateStatus({
×
486
            status: ResponseStatus.error,
×
487
            response: error,
×
488
            helperText: "snackbarMessages.error.helperText.submitDoiError",
×
489
          })
×
490
        )
×
491
      )
×
492
  }
×
493

494
  /*
495
   * Logic for auto-save feature.
496
   * We use setDraftAutoSaveAllowed state change to render form before save.
497
   * This helps with getting current accession ID and form data without rendering on every timer increment.
498
   */
499
  const startTimer = () => {
14✔
500
    autoSaveTimer.current = setInterval(() => {
2✔
501
      timer = timer + 1
×
502
      if (timer >= 60) {
×
503
        setDraftAutoSaveAllowed(true)
×
504
      }
×
505
    }, 1000)
2✔
506
  }
2✔
507

508
  const resetTimer = () => {
14✔
509
    setDraftAutoSaveAllowed(false)
9✔
510
    clearInterval(autoSaveTimer.current as NodeJS.Timeout)
9✔
511
    timer = 0
9✔
512
  }
9✔
513

514
  useEffect(() => {
14✔
515
    if (alert) resetTimer()
7!
516

517
    if (draftAutoSaveAllowed) {
7!
518
      handleSaveDraft()
×
519
      resetTimer()
×
520
    }
×
521
  }, [draftAutoSaveAllowed, alert])
14✔
522

523
  const keyHandler = () => {
14✔
524
    resetTimer()
2✔
525

526
    // Prevent auto save from DOI form and template dialog
527
    if (
2✔
528
      currentObject?.status !== ObjectStatus.template &&
2✔
529
      objectType !== (ObjectTypes.study || "datacite")
2!
530
    )
531
      startTimer()
2✔
532
  }
2✔
533

534
  useEffect(() => {
14✔
535
    window.addEventListener("keydown", keyHandler)
7✔
536
    return () => {
7✔
537
      resetTimer()
7✔
538
      window.removeEventListener("keydown", keyHandler)
7✔
539
    }
7✔
540
  }, [])
14✔
541

542
  const emptyFormError = () => {
14✔
543
    dispatch(
×
544
      updateStatus({
×
545
        status: ResponseStatus.info,
×
546
        helperText: "An empty form cannot be saved. Please fill in the form before saving it.",
×
547
      })
×
548
    )
×
549
  }
×
550

551
  const handleSaveTemplate = async () => {
14✔
552
    const cleanedValues = getCleanedValues()
×
553

554
    if (!isFormCleanedValuesEmpty(cleanedValues)) {
×
555
      const index =
×
556
        templates?.findIndex(
×
557
          (item: { accessionId: string }) => item.accessionId === currentObject.accessionId
×
558
        ) || 0
×
559
      const response = await templateAPI.patchTemplateFromJSON(
×
560
        objectType,
×
561
        currentObject.accessionId,
×
562
        cleanedValues,
×
563
        index
×
564
      )
×
565

566
      const displayTitle = getObjectDisplayTitle(
×
567
        objectType,
×
568
        cleanedValues as unknown as ObjectDisplayValues
×
569
      )
×
570

571
      if (response.ok) {
×
572
        closeDialog()
×
573
        dispatch(
×
574
          updateTemplateDisplayTitle({
×
575
            accessionId: currentObject.accessionId,
×
576
            displayTitle: displayTitle,
×
577
          })
×
578
        )
×
579

580
        dispatch(
×
581
          updateStatus({
×
582
            status: ResponseStatus.success,
×
583
            response: response,
×
584
            helperText: "",
×
585
          })
×
586
        )
×
587
      } else {
×
588
        dispatch(
×
589
          updateStatus({
×
590
            status: ResponseStatus.error,
×
591
            response: response,
×
592
            helperText: "Cannot save template",
×
593
          })
×
594
        )
×
595
      }
×
596
    } else {
×
597
      emptyFormError()
×
598
    }
×
599
  }
×
600

601
  /*
602
   * Update or save new draft depending on object status
603
   */
604
  const handleSaveDraft = async () => {
14✔
605
    resetTimer()
×
606
    const cleanedValues = getCleanedValues()
×
607

608
    if (!isFormCleanedValuesEmpty(cleanedValues)) {
×
609
      const handleSave = await saveDraftHook({
×
610
        accessionId: currentObject.accessionId || currentObject.objectId,
×
611
        objectType: objectType,
×
612
        objectStatus: currentObject.status,
×
613
        submission: submission,
×
614
        values: cleanedValues,
×
615
        dispatch: dispatch,
×
616
      })
×
617

618
      if (handleSave.ok && currentObject?.status !== ObjectStatus.submitted) {
×
619
        setCurrentObjectId(handleSave.data.accessionId)
×
620
        const clone = cloneDeep(currentObject)
×
621
        dispatch(
×
622
          setCurrentObject({
×
623
            ...clone,
×
624
            status: currentObject.status || ObjectStatus.draft,
×
625
            accessionId: handleSave.data.accessionId,
×
626
          })
×
627
        )
×
628
        dispatch(resetDraftStatus())
×
629
      }
×
630
    } else {
×
631
      emptyFormError()
×
632
    }
×
633
  }
×
634

635
  const handleXMLModalOpen = () => {
14✔
636
    dispatch(setXMLModalOpen())
×
637
  }
×
638

639
  const handleDeleteForm = async () => {
14✔
640
    if (currentObjectId) {
×
641
      try {
×
642
        await dispatch(
×
643
          deleteObjectFromSubmission(currentObject.status, currentObjectId, objectType)
×
644
        )
×
645
        handleReset()
×
646
        handleChange()
×
647
        dispatch(resetCurrentObject())
×
648

649
        // Delete fileType that is equivalent to deleted object (for Run and Analysis cases)
650
        if (objectType === ObjectTypes.analysis || objectType === ObjectTypes.run) {
×
651
          dispatch(deleteFileType(currentObjectId))
×
652
        }
×
653
      } catch (error) {
×
654
        dispatch(
×
655
          updateStatus({
×
656
            status: ResponseStatus.error,
×
657
            response: error,
×
658
            helperText: "snackbarMessages.error.helperText.deleteObjectFromSubmission",
×
659
          })
×
660
        )
×
661
      }
×
662
    }
×
663
  }
×
664

665
  const handleReset = () => {
14✔
666
    methods.reset({ undefined })
×
667
    setCurrentObjectId(null)
×
668
  }
×
669

670
  return (
14✔
671
    <FormProvider {...methods}>
14✔
672
      <CustomCardHeader
14✔
673
        hasSubmittedObject={hasSubmittedObject}
14✔
674
        hasDraftObject={hasDraftObject}
14✔
675
        objectType={objectType}
14✔
676
        currentObject={currentObject}
14✔
677
        refForm="hook-form"
14✔
678
        onClickSaveDraft={() => handleSaveDraft()}
14✔
679
        onClickUpdateTemplate={() => handleSaveTemplate()}
14✔
680
        onClickSubmit={() => resetTimer()}
14✔
681
        onClickSaveDOI={methods.handleSubmit(async data => handleDOISubmit(data as DoiFormDetails))}
14✔
682
        onClickCloseDialog={() => closeDialog()}
14✔
683
        onClickClearForm={() => handleClearForm()}
14✔
684
        onOpenXMLModal={() => handleXMLModalOpen()}
14✔
685
        onDeleteForm={() => handleDeleteForm()}
14✔
686
      />
687

688
      <Form
14✔
689
        id="hook-form"
14✔
690
        onChange={() => handleChange()}
14✔
691
        onSubmit={methods.handleSubmit(onSubmit)}
14✔
692
        ref={formRef as RefObject<HTMLFormElement>}
14✔
693
        onReset={handleReset}
14✔
694
      >
695
        <Box>{JSONSchemaParser.buildFields(formSchema)}</Box>
14✔
696
      </Form>
14✔
697
    </FormProvider>
14✔
698
  )
699
}
14✔
700

701
/*
702
 * Container for json schema based form. Handles json schema loading, form rendering, form submitting and error/success alerts.
703
 */
704
const WizardFillObjectDetailsForm = (props: { closeDialog?: () => void; formRef?: FormRef }) => {
1✔
705
  const { closeDialog, formRef } = props
23✔
706
  const dispatch = useAppDispatch()
23✔
707

708
  const objectType = useAppSelector(state => state.objectType)
23✔
709
  const submission = useAppSelector(state => state.submission)
23✔
710
  const currentObject = useAppSelector(state => state.currentObject)
23✔
711
  const locale = useAppSelector(state => state.locale)
23✔
712
  const openedXMLModal = useAppSelector(state => state.openedXMLModal)
23✔
713

714
  const { hasDraftObject, hasSubmittedObject } = checkObjectStatus(submission, objectType)
23✔
715

716
  // States that will update in useEffect()
717
  const [states, setStates] = useState({
23✔
718
    error: false,
23✔
719
    helperText: "",
23✔
720
    formSchema: {},
23✔
721
    validationSchema: {} as FormObject,
23✔
722
    isLoading: true,
23✔
723
  })
23✔
724
  const resolver = WizardAjvResolver(states.validationSchema, locale)
23✔
725
  const methods = useForm({ mode: "onBlur", resolver })
23✔
726

727
  const [submitting, setSubmitting] = useState(false)
23✔
728

729
  /*
730
   * Fetch json schema from either session storage or API, set schema and dereferenced version to component state.
731
   */
732
  useEffect(() => {
23✔
733
    const fetchSchema = async () => {
8✔
734
      const schema: string | null = sessionStorage.getItem(`cached_${objectType}_schema`)
8✔
735
      let parsedSchema: FormObject
8✔
736
      const ajv = new Ajv2020()
8✔
737

738
      if (!schema || !ajv.validateSchema(JSON.parse(schema))) {
8✔
739
        const response = await schemaAPIService.getSchemaByObjectType(objectType)
1!
740
        if (response.ok) {
×
741
          parsedSchema = response.data
×
742
          sessionStorage.setItem(`cached_${objectType}_schema`, JSON.stringify(parsedSchema))
×
743
        } else {
×
744
          setStates({
×
745
            ...states,
×
746
            error: true,
×
747
            helperText: "Unfortunately an error happened while catching form fields",
×
748
            isLoading: false,
×
749
          })
×
750
          return
×
751
        }
×
752
      } else {
8✔
753
        parsedSchema = JSON.parse(schema)
7✔
754
      }
7✔
755

756
      // Dereference Schema and link AccessionIds to equivalent objects
757
      let dereferencedSchema: Promise<FormObject> = await dereferenceSchema(
7✔
758
        parsedSchema as FormObject
7✔
759
      )
7✔
760

761
      dereferencedSchema = getLinkedDereferencedSchema(
7✔
762
        currentObject,
7✔
763
        parsedSchema.title.toLowerCase(),
7✔
764
        dereferencedSchema,
7✔
765
        submission.metadataObjects,
7✔
766
        analysisAccessionIds
7✔
767
      )
7✔
768

769
      setStates({
7✔
770
        ...states,
7✔
771
        formSchema: dereferencedSchema,
7✔
772
        validationSchema: parsedSchema,
7✔
773
        isLoading: false,
7✔
774
      })
7✔
775
    }
8✔
776

777
    // In case of there is object type, and Summary does not have schema
778
    if (objectType.length && objectType!=="Summary") fetchSchema()
8✔
779

780
    // Reset current object in state on unmount
781
    return () => {
8✔
782
      dispatch(resetDraftStatus())
8✔
783
    }
8✔
784
  }, [objectType])
23✔
785

786
  // All Analysis AccessionIds
787
  const analysisAccessionIds = getAccessionIds(ObjectTypes.analysis, submission.metadataObjects)
23✔
788

789
  useEffect(() => {
23✔
790
    if (ObjectTypes.analysis) {
8✔
791
      if (analysisAccessionIds?.length > 0) {
8!
792
        // Link other Analysis AccessionIds to current Analysis form
793
        setStates(prevState => {
×
794
          return set(
×
795
            prevState,
×
796
            `formSchema.properties.analysisRef.items.properties.accessionId.enum`,
×
797
            analysisAccessionIds.filter(id => id !== currentObject?.accessionId)
×
798
          )
×
799
        })
×
800
      }
×
801
    }
8✔
802
  }, [currentObject?.accessionId, analysisAccessionIds?.length])
23✔
803

804
  /*
805
   * Submit form with cleaned values and check for response errors
806
   */
807
  const onSubmit = (data: Record<string, unknown>) => {
23✔
808
    setSubmitting(true)
×
809

810
    // Handle submitted object update
811
    const patchObject = async () => {
×
812
      const accessionId = data.accessionId as string
×
813
      const cleanedValues = JSONSchemaParser.cleanUpFormValues(data)
×
814
      try {
×
815
        const response = await objectAPIService.patchFromJSON(
×
816
          objectType,
×
817
          accessionId,
×
818
          cleanedValues
×
819
        )
×
820

821
        patchHandler(
×
822
          response,
×
823
          submission,
×
824
          currentObject.accessionId,
×
825
          objectType,
×
826
          cleanedValues,
×
827
          dispatch
×
828
        )
×
829
        dispatch(resetCurrentObject())
×
830
        methods.reset({ undefined })
×
831
        // Dispatch fileTypes if object is Run or Analysis
832
        if (objectType === ObjectTypes.run || objectType === ObjectTypes.analysis) {
×
833
          const objectWithFileTypes = getNewUniqueFileTypes(
×
834
            accessionId,
×
835
            cleanedValues as FormDataFiles
×
836
          )
×
837
          objectWithFileTypes ? dispatch(setFileTypes(objectWithFileTypes)) : null
×
838
        }
×
839
      } catch (error) {
×
840
        dispatch(
×
841
          updateStatus({
×
842
            status: ResponseStatus.error,
×
843
            response: error,
×
844
            helperText: "Unexpected error when modifying object",
×
845
          })
×
846
        )
×
847
      }
×
848

849
      setSubmitting(false)
×
850
    }
×
851

852
    // Either patch object or submit a new object
853
    if (data.status === ObjectStatus.submitted) {
×
854
      patchObject()
×
855
    } else {
×
856
      submitObjectHook(data, submission.submissionId, objectType, dispatch)
×
857
        .then(() => {
×
858
          setSubmitting(false)
×
859
          methods.reset({ undefined })
×
860
        })
×
861
        .catch(err => console.error(err))
×
862
    }
×
863
  }
×
864

865
  if (states.isLoading) return <CircularProgress />
23✔
866
  // Schema validation error differs from response status handler
867
  if (states.error)
15✔
868
    return (
15!
869
      <CustomAlert severity="error" icon={<CancelIcon sx={{ fontSize: "2rem" }} />}>
×
870
        <AlertMessage>{states.helperText}</AlertMessage>
×
871
      </CustomAlert>
✔
872
    )
873

874
  return (
15✔
875
    <>
15✔
876
      <GlobalStyles styles={{ ".MuiContainer-root": { maxWidth: "100% !important" } }} />
15✔
877
      <Container sx={{ m: 0, p: 0, width: "100%", boxSizing: "border-box" }} maxWidth={false}>
15✔
878
        <FormContent
15✔
879
          formSchema={states.formSchema as FormObject}
15✔
880
          methods={methods}
15✔
881
          onSubmit={onSubmit as SubmitHandler<FieldValues>}
15✔
882
          objectType={objectType}
15✔
883
          hasDraftObject={hasDraftObject}
15✔
884
          hasSubmittedObject={hasSubmittedObject}
15✔
885
          submission={submission}
15✔
886
          currentObject={currentObject}
15✔
887
          key={currentObject?.accessionId || submission.submissionId}
15✔
888
          closeDialog={closeDialog || (() => {})}
15✔
889
          formRef={formRef}
23✔
890
        />
891
        {submitting && <LinearProgress />}
23!
892
        <WizardXMLUploadModal
23✔
893
          open={openedXMLModal}
23✔
894
          handleClose={() => {
23✔
895
            dispatch(resetXMLModalOpen())
×
896
          }}
×
897
        />
898
      </Container>
23✔
899
    </>
23✔
900
  )
901
}
23✔
902

903
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