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

CSCfi / metadata-submitter-frontend / 15132822209

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

push

github

Hang Le
Feature/fix formatting (merge commit)

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

* Update precommit for husky v9

* Fix formatting script to include all files

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

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

656 of 963 branches covered (68.12%)

Branch coverage included in aggregate %.

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

5 existing lines in 4 files now uncovered.

6353 of 13705 relevant lines covered (46.36%)

4.25 hits per line

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

57.28
/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 CustomAlert = styled(Alert)(({ theme, severity }) => ({
1✔
69
  backgroundColor: theme.palette.background.paper,
×
70
  borderLeft: `1.25rem solid ${
×
71
    severity === "error"
×
72
      ? theme.palette.error.main
×
73
      : severity === "info"
×
74
        ? theme.palette.error.light
×
75
        : theme.palette.success.light
×
76
  }`,
77
  borderTop: `0.25rem solid ${
×
78
    severity === "error"
×
79
      ? theme.palette.error.main
×
80
      : severity === "info"
×
81
        ? theme.palette.error.light
×
82
        : theme.palette.success.light
×
83
  }`,
84
  borderRight: `0.25rem solid ${
×
85
    severity === "error"
×
86
      ? theme.palette.error.main
×
87
      : severity === "info"
×
88
        ? theme.palette.error.light
×
89
        : theme.palette.success.light
×
90
  }`,
91
  borderBottom: `0.25rem solid ${
×
92
    severity === "error"
×
93
      ? theme.palette.error.main
×
94
      : severity === "info"
×
95
        ? theme.palette.error.light
×
96
        : theme.palette.success.light
×
97
  }`,
98
  "& .MuiAlert-icon": {
×
99
    display: "flex",
×
100
    alignItems: "center",
×
101
    justifyContent: "center",
×
102
    padding: "0 0.5rem",
×
103
  },
×
104
  color: theme.palette.secondary.main,
×
105
  lineHeight: "1.75",
×
106
  boxShadow: "0 0.25rem 0.625rem rgba(0, 0, 0, 0.2)",
×
107
  position: "relative",
×
108
  padding: "1rem",
×
109
  display: "flex",
×
110
  justifyContent: "flex-start",
×
111
  alignItems: "center",
×
112
  width: "40%",
×
113
  margin: "6.25rem auto 0 auto",
×
114
}))
1✔
115

116
const AlertMessage = styled(Typography)({
1✔
117
  fontWeight: "bold",
1✔
118
})
1✔
119

120
const ButtonGroup = styled(Box)(({ theme }) => ({
1✔
121
  display: "flex",
16✔
122
  gridTemplateColumns: "repeat(3, 1fr)",
16✔
123
  columnGap: "2rem",
16✔
124
  marginLeft: "2rem",
16✔
125
  marginRight: "3rem",
16✔
126
  "& button": {
16✔
127
    backgroundColor: theme.palette.primary.main,
16✔
128
    color: theme.palette.common.white,
16✔
129
    height: "4.5rem",
16✔
130
    width: "16rem",
16✔
131
  },
16✔
132
}))
1✔
133

134
const Form = styled("form")(({ theme }) => ({
1✔
135
  ...theme.form,
16✔
136
}))
1✔
137

138
type CustomCardHeaderProps = {
139
  hasSubmittedObject: boolean
140
  hasDraftObject: boolean
141
  objectType: string
142
  currentObject: ObjectDetails
143
  onClickSaveDraft: () => Promise<void>
144
  onClickUpdateTemplate: () => Promise<void>
145
  onClickSubmit: () => void
146
  onClickSaveDOI: () => Promise<void>
147
  onClickCloseDialog: () => void
148
  onClickClearForm: () => void
149
  onOpenXMLModal: () => void
150
  onDeleteForm: () => void
151
  refForm: string
152
}
153

154
type FormContentProps = {
155
  methods: UseFormReturn
156
  formSchema: FormObject
157
  onSubmit: SubmitHandler<FieldValues>
158
  objectType: string
159
  submission: SubmissionDetailsWithId
160
  hasDraftObject: boolean
161
  hasSubmittedObject: boolean
162
  currentObject: ObjectDetails & { objectId: string; [key: string]: unknown }
163
  closeDialog: () => void
164
  formRef?: FormRef
165
}
166

167
/*
168
 * Create header for form card with button to close the card
169
 */
170
const CustomCardHeader = (props: CustomCardHeaderProps) => {
1✔
171
  const {
16✔
172
    hasSubmittedObject,
16✔
173
    hasDraftObject,
16✔
174
    objectType,
16✔
175
    currentObject,
16✔
176
    refForm,
16✔
177
    onClickSaveDraft,
16✔
178
    onClickUpdateTemplate,
16✔
179
    onClickSubmit,
16✔
180
    onClickSaveDOI,
16✔
181
    onClickCloseDialog,
16✔
182
    onClickClearForm,
16✔
183
    onOpenXMLModal,
16✔
184
    onDeleteForm,
16✔
185
  } = props
16✔
186

187
  const focusTarget = useRef<HTMLButtonElement>(null)
16✔
188
  const shouldFocus = useAppSelector(state => state.focus)
16✔
189
  const { t } = useTranslation()
16✔
190

191
  useEffect(() => {
16✔
192
    if (shouldFocus && focusTarget.current) focusTarget.current.focus()
7!
193
  }, [shouldFocus])
16✔
194

195
  const templateButtonGroup = (
16✔
196
    <ButtonGroup>
16✔
197
      <Button
16✔
198
        type="submit"
16✔
199
        variant="contained"
16✔
200
        aria-label="save form as draft"
16✔
201
        size="small"
16✔
202
        onClick={onClickUpdateTemplate}
16✔
203
      >
204
        Update template
205
      </Button>
16✔
206
      <Button variant="contained" aria-label="clear form" size="small" onClick={onClickCloseDialog}>
16✔
207
        Close
208
      </Button>
16✔
209
    </ButtonGroup>
16✔
210
  )
211

212
  const buttonGroup = (
16✔
213
    <Box display="flex">
16✔
214
      <WizardOptions
16✔
215
        objectType={objectType}
16✔
216
        onClearForm={onClickClearForm}
16✔
217
        onOpenXMLModal={onOpenXMLModal}
16✔
218
        onDeleteForm={onDeleteForm}
16✔
219
        disableUploadXML={
16✔
220
          objectType === ObjectTypes.study && (hasDraftObject || hasSubmittedObject)
16✔
221
        }
16✔
222
      />
223
      <ButtonGroup>
16✔
224
        <Button
16✔
225
          variant="contained"
16✔
226
          aria-label="save form as draft"
16✔
227
          size="small"
16✔
228
          onClick={onClickSaveDraft}
16✔
229
          data-testid="form-draft"
16✔
230
          disabled={objectType === ObjectTypes.study && hasSubmittedObject}
16✔
231
        >
232
          {(objectType === ObjectTypes.study && hasDraftObject) ||
16✔
233
          currentObject?.status === ObjectStatus.draft
16!
234
            ? t("formActions.updateDraft")
×
235
            : t("formActions.saveAsDraft")}
16✔
236
        </Button>
16✔
237
        <Button
16✔
238
          variant="contained"
16✔
239
          aria-label="submit form"
16✔
240
          size="small"
16✔
241
          type="submit"
16✔
242
          onClick={onClickSubmit}
16✔
243
          form={refForm}
16✔
244
          data-testid="form-ready"
16✔
245
          disabled={
16✔
246
            objectType === ObjectTypes.study && hasSubmittedObject && !currentObject.accessionId
16✔
247
          }
248
        >
249
          {(objectType === ObjectTypes.study && hasSubmittedObject) ||
16✔
250
          currentObject?.status === ObjectStatus.submitted
5✔
251
            ? t("formActions.update")
11✔
252
            : t("formActions.markAsReady")}
5✔
253
        </Button>
16✔
254
      </ButtonGroup>
16✔
255
    </Box>
16✔
256
  )
257

258
  const doiButtonGroup = (
16✔
259
    <Box display="flex">
16✔
260
      <WizardOptions
16✔
261
        objectType={objectType}
16✔
262
        onClearForm={onClickClearForm}
16✔
263
        onOpenXMLModal={onOpenXMLModal}
16✔
264
        onDeleteForm={onDeleteForm}
16✔
265
      />
266
      <ButtonGroup>
16✔
267
        <Button
16✔
268
          variant="contained"
16✔
269
          aria-label="save Datacite"
16✔
270
          size="small"
16✔
271
          onClick={onClickSaveDOI}
16✔
272
          data-testid="form-datacite"
16✔
273
        >
274
          {t("save")}
16✔
275
        </Button>
16✔
276
      </ButtonGroup>
16✔
277
    </Box>
16✔
278
  )
279

280
  return (
16✔
281
    <WizardStepContentHeader
16✔
282
      action={
16✔
283
        currentObject?.status === ObjectStatus.template
16!
284
          ? templateButtonGroup
×
285
          : objectType === ObjectTypes.datacite
16!
286
            ? doiButtonGroup
×
287
            : buttonGroup
16✔
288
      }
16✔
289
    />
290
  )
291
}
16✔
292

293
/*
294
 * Draft save and object patch use both same response handler
295
 */
296
const patchHandler = (
1✔
297
  response: ApiResponse<unknown>,
×
298
  submission: SubmissionDetailsWithId,
×
299
  accessionId: string,
×
300
  objectType: string,
×
301
  cleanedValues: Record<string, unknown>,
×
NEW
302
  dispatch: (reducer: unknown) => void
×
303
) => {
×
304
  if (response.ok) {
×
305
    dispatch(
×
306
      replaceObjectInSubmission(
×
307
        accessionId,
×
308
        {
×
309
          submissionType: ObjectSubmissionTypes.form,
×
310
          displayTitle: getObjectDisplayTitle(objectType, cleanedValues as ObjectDisplayValues),
×
311
        },
×
NEW
312
        ObjectStatus.submitted
×
NEW
313
      )
×
314
    )
×
315
    dispatch(resetDraftStatus())
×
316
    dispatch(
×
317
      updateStatus({
×
318
        status: ResponseStatus.success,
×
319
        response: response,
×
320
        helperText: "",
×
NEW
321
      })
×
322
    )
×
323
  } else {
×
324
    dispatch(
×
325
      updateStatus({
×
326
        status: ResponseStatus.error,
×
327
        response: response,
×
328
        helperText: "Unexpected error",
×
NEW
329
      })
×
330
    )
×
331
  }
×
332
}
×
333

334
/*
335
 * Return react-hook-form based form which is rendered from schema and checked against resolver. Set default values when continuing draft
336
 */
337
const FormContent = ({
1✔
338
  methods,
16✔
339
  formSchema,
16✔
340
  onSubmit,
16✔
341
  objectType,
16✔
342
  hasDraftObject,
16✔
343
  hasSubmittedObject,
16✔
344
  submission,
16✔
345
  currentObject,
16✔
346
  closeDialog,
16✔
347
  formRef,
16✔
348
}: FormContentProps) => {
16✔
349
  const dispatch = useAppDispatch()
16✔
350

351
  const alert = useAppSelector(state => state.alert)
16✔
352
  const clearForm = useAppSelector(state => state.clearForm)
16✔
353

354
  const templates = useAppSelector(state => state.templates)
16✔
355

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

359
  const autoSaveTimer: { current: NodeJS.Timeout | null } = useRef(null)
16✔
360
  let timer = 0
16✔
361

362
  // Set form default values
363
  useEffect(() => {
16✔
364
    methods.reset(currentObject)
7✔
365
  }, [currentObject?.accessionId])
16✔
366

367
  useEffect(() => {
16✔
368
    dispatch(setClearForm(false))
7✔
369
  }, [clearForm])
16✔
370

371
  // Check if form has been edited
372
  useEffect(() => {
16✔
373
    checkDirty()
8✔
374
  }, [methods.formState.isDirty])
16✔
375

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

378
  useEffect(() => {
16✔
379
    // Delete draft form ONLY if the form was successfully submitted
380
    if (isSubmitSuccessful) {
7!
381
      if (
×
382
        currentObject?.status === ObjectStatus.draft &&
×
383
        currentObjectId &&
×
384
        Object.keys(currentObject).length > 0
×
385
      ) {
×
386
        handleDeleteForm()
×
387
      }
×
388
    }
×
389
  }, [isSubmitSuccessful])
16✔
390

391
  const handleClearForm = () => {
16✔
392
    resetTimer()
×
393
    methods.reset({ undefined })
×
394
    dispatch(setClearForm(true))
×
395
    dispatch(
×
396
      setCurrentObject({
×
397
        objectId: currentObjectId,
×
398
        status: currentObject.status,
×
NEW
399
      })
×
400
    )
×
401
  }
×
402

403
  // Check if the form is empty
404
  const isFormCleanedValuesEmpty = (cleanedValues: {
16✔
405
    [x: string]: unknown
406
    [x: number]: unknown
407
    accessionId?: string
408
    lastModified?: string
409
    objectType?: string
410
    status?: string
411
    title?: string
412
    submissionType?: string
413
  }) => {
2✔
414
    return Object.keys(cleanedValues).filter(val => val !== "index").length === 0
2✔
415
  }
2✔
416

417
  const checkDirty = () => {
16✔
418
    const isFormTouched = () => {
10✔
419
      return Object.keys(methods.formState.dirtyFields).length > 0
10✔
420
    }
10✔
421

422
    if (isFormTouched()) {
10✔
423
      dispatch(setDraftStatus("notSaved"))
2✔
424
    } else dispatch(resetDraftStatus())
10✔
425
  }
10✔
426

427
  const getCleanedValues = () =>
16✔
428
    JSONSchemaParser.cleanUpFormValues(methods.getValues()) as ObjectDetails
2✔
429

430
  // Draft data is set to state on every change to form
431
  const handleChange = () => {
16✔
432
    clearForm ? dispatch(setClearForm(false)) : null
2!
433
    const clone = cloneDeep(currentObject)
2✔
434
    const values = getCleanedValues()
2✔
435

436
    if (clone && !isFormCleanedValuesEmpty(values)) {
2✔
437
      Object.keys(values).forEach(item => (clone[item] = values[item]))
2✔
438

439
      !currentObject.accessionId && currentObjectId
2!
440
        ? dispatch(
×
441
            setCurrentObject({
×
442
              ...clone,
×
443
              cleanedValues: values,
×
444
              status: currentObject.status || ObjectStatus.draft,
×
445
              objectId: currentObjectId,
×
NEW
446
            })
×
447
          )
×
448
        : dispatch(setCurrentObject({ ...clone, cleanedValues: values }))
2✔
449
      checkDirty()
2✔
450
    } else {
2!
451
      dispatch(resetDraftStatus())
×
452
      resetTimer()
×
453
    }
×
454
  }
2✔
455

456
  const handleDOISubmit = async (data: DoiFormDetails) => {
16✔
457
    dispatch(addDoiInfoToSubmission(submission.submissionId, data))
×
458
      .then(() => {
×
459
        dispatch(resetAutocompleteField())
×
460
        dispatch(resetCurrentObject())
×
461
        dispatch(
×
462
          updateStatus({
×
463
            status: ResponseStatus.success,
×
464
            helperText: "snackbarMessages.success.doi.saved",
×
NEW
465
          })
×
466
        )
×
467
      })
×
468
      .catch(error =>
×
469
        dispatch(
×
470
          updateStatus({
×
471
            status: ResponseStatus.error,
×
472
            response: error,
×
473
            helperText: "snackbarMessages.error.helperText.submitDoiError",
×
NEW
474
          })
×
NEW
475
        )
×
476
      )
×
477
  }
×
478

479
  /*
480
   * Logic for auto-save feature.
481
   * We use setDraftAutoSaveAllowed state change to render form before save.
482
   * This helps with getting current accession ID and form data without rendering on every timer increment.
483
   */
484
  const startTimer = () => {
16✔
485
    autoSaveTimer.current = setInterval(() => {
2✔
486
      timer = timer + 1
×
487
      if (timer >= 60) {
×
488
        setDraftAutoSaveAllowed(true)
×
489
      }
×
490
    }, 1000)
2✔
491
  }
2✔
492

493
  const resetTimer = () => {
16✔
494
    setDraftAutoSaveAllowed(false)
9✔
495
    clearInterval(autoSaveTimer.current as NodeJS.Timeout)
9✔
496
    timer = 0
9✔
497
  }
9✔
498

499
  useEffect(() => {
16✔
500
    if (alert) resetTimer()
7!
501

502
    if (draftAutoSaveAllowed) {
7!
503
      handleSaveDraft()
×
504
      resetTimer()
×
505
    }
×
506
  }, [draftAutoSaveAllowed, alert])
16✔
507

508
  const keyHandler = () => {
16✔
509
    resetTimer()
2✔
510

511
    // Prevent auto save from DOI form and template dialog
512
    if (
2✔
513
      currentObject?.status !== ObjectStatus.template &&
2✔
514
      objectType !== (ObjectTypes.study || ObjectTypes.datacite)
2!
515
    )
516
      startTimer()
2✔
517
  }
2✔
518

519
  useEffect(() => {
16✔
520
    window.addEventListener("keydown", keyHandler)
7✔
521
    return () => {
7✔
522
      resetTimer()
7✔
523
      window.removeEventListener("keydown", keyHandler)
7✔
524
    }
7✔
525
  }, [])
16✔
526

527
  const emptyFormError = () => {
16✔
528
    dispatch(
×
529
      updateStatus({
×
530
        status: ResponseStatus.info,
×
531
        helperText: "An empty form cannot be saved. Please fill in the form before saving it.",
×
NEW
532
      })
×
533
    )
×
534
  }
×
535

536
  const handleSaveTemplate = async () => {
16✔
537
    const cleanedValues = getCleanedValues()
×
538

539
    if (!isFormCleanedValuesEmpty(cleanedValues)) {
×
540
      const index =
×
541
        templates?.findIndex(
×
NEW
542
          (item: { accessionId: string }) => item.accessionId === currentObject.accessionId
×
543
        ) || 0
×
544
      const response = await templateAPI.patchTemplateFromJSON(
×
545
        objectType,
×
546
        currentObject.accessionId,
×
547
        cleanedValues,
×
NEW
548
        index
×
549
      )
×
550

551
      const displayTitle = getObjectDisplayTitle(
×
552
        objectType,
×
NEW
553
        cleanedValues as unknown as ObjectDisplayValues
×
554
      )
×
555

556
      if (response.ok) {
×
557
        closeDialog()
×
558
        dispatch(
×
559
          updateTemplateDisplayTitle({
×
560
            accessionId: currentObject.accessionId,
×
561
            displayTitle: displayTitle,
×
NEW
562
          })
×
563
        )
×
564

565
        dispatch(
×
566
          updateStatus({
×
567
            status: ResponseStatus.success,
×
568
            response: response,
×
569
            helperText: "",
×
NEW
570
          })
×
571
        )
×
572
      } else {
×
573
        dispatch(
×
574
          updateStatus({
×
575
            status: ResponseStatus.error,
×
576
            response: response,
×
577
            helperText: "Cannot save template",
×
NEW
578
          })
×
579
        )
×
580
      }
×
581
    } else {
×
582
      emptyFormError()
×
583
    }
×
584
  }
×
585

586
  /*
587
   * Update or save new draft depending on object status
588
   */
589
  const handleSaveDraft = async () => {
16✔
590
    resetTimer()
×
591
    const cleanedValues = getCleanedValues()
×
592

593
    if (!isFormCleanedValuesEmpty(cleanedValues)) {
×
594
      const handleSave = await saveDraftHook({
×
595
        accessionId: currentObject.accessionId || currentObject.objectId,
×
596
        objectType: objectType,
×
597
        objectStatus: currentObject.status,
×
598
        submission: submission,
×
599
        values: cleanedValues,
×
600
        dispatch: dispatch,
×
601
      })
×
602

603
      if (handleSave.ok && currentObject?.status !== ObjectStatus.submitted) {
×
604
        setCurrentObjectId(handleSave.data.accessionId)
×
605
        const clone = cloneDeep(currentObject)
×
606
        dispatch(
×
607
          setCurrentObject({
×
608
            ...clone,
×
609
            status: currentObject.status || ObjectStatus.draft,
×
610
            accessionId: handleSave.data.accessionId,
×
NEW
611
          })
×
612
        )
×
613
        dispatch(resetDraftStatus())
×
614
      }
×
615
    } else {
×
616
      emptyFormError()
×
617
    }
×
618
  }
×
619

620
  const handleXMLModalOpen = () => {
16✔
621
    dispatch(setXMLModalOpen())
×
622
  }
×
623

624
  const handleDeleteForm = async () => {
16✔
625
    if (currentObjectId) {
×
626
      try {
×
627
        await dispatch(
×
NEW
628
          deleteObjectFromSubmission(currentObject.status, currentObjectId, objectType)
×
629
        )
×
630
        handleReset()
×
631
        handleChange()
×
632
        dispatch(resetCurrentObject())
×
633

634
        // Delete fileType that is equivalent to deleted object (for Run and Analysis cases)
635
        if (objectType === ObjectTypes.analysis || objectType === ObjectTypes.run) {
×
636
          dispatch(deleteFileType(currentObjectId))
×
637
        }
×
638
      } catch (error) {
×
639
        dispatch(
×
640
          updateStatus({
×
641
            status: ResponseStatus.error,
×
642
            response: error,
×
643
            helperText: "snackbarMessages.error.helperText.deleteObjectFromSubmission",
×
NEW
644
          })
×
645
        )
×
646
      }
×
647
    }
×
648
  }
×
649

650
  const handleReset = () => {
16✔
651
    methods.reset({ undefined })
×
652
    setCurrentObjectId(null)
×
653
  }
×
654

655
  return (
16✔
656
    <FormProvider {...methods}>
16✔
657
      <CustomCardHeader
16✔
658
        hasSubmittedObject={hasSubmittedObject}
16✔
659
        hasDraftObject={hasDraftObject}
16✔
660
        objectType={objectType}
16✔
661
        currentObject={currentObject}
16✔
662
        refForm="hook-form"
16✔
663
        onClickSaveDraft={() => handleSaveDraft()}
16✔
664
        onClickUpdateTemplate={() => handleSaveTemplate()}
16✔
665
        onClickSubmit={() => resetTimer()}
16✔
666
        onClickSaveDOI={methods.handleSubmit(async data => handleDOISubmit(data as DoiFormDetails))}
16✔
667
        onClickCloseDialog={() => closeDialog()}
16✔
668
        onClickClearForm={() => handleClearForm()}
16✔
669
        onOpenXMLModal={() => handleXMLModalOpen()}
16✔
670
        onDeleteForm={() => handleDeleteForm()}
16✔
671
      />
672

673
      <Form
16✔
674
        id="hook-form"
16✔
675
        onChange={() => handleChange()}
16✔
676
        onSubmit={methods.handleSubmit(onSubmit)}
16✔
677
        ref={formRef as RefObject<HTMLFormElement>}
16✔
678
        onReset={handleReset}
16✔
679
      >
680
        <Box>{JSONSchemaParser.buildFields(formSchema)}</Box>
16✔
681
      </Form>
16✔
682
    </FormProvider>
16✔
683
  )
684
}
16✔
685

686
/*
687
 * Container for json schema based form. Handles json schema loading, form rendering, form submitting and error/success alerts.
688
 */
689
const WizardFillObjectDetailsForm = (props: { closeDialog?: () => void; formRef?: FormRef }) => {
1✔
690
  const { closeDialog, formRef } = props
33✔
691
  const dispatch = useAppDispatch()
33✔
692

693
  const objectType = useAppSelector(state => state.objectType)
33✔
694
  const submission = useAppSelector(state => state.submission)
33✔
695
  const currentObject = useAppSelector(state => state.currentObject)
33✔
696
  const locale = useAppSelector(state => state.locale)
33✔
697
  const openedXMLModal = useAppSelector(state => state.openedXMLModal)
33✔
698

699
  const { hasDraftObject, hasSubmittedObject } = checkObjectStatus(submission, objectType)
33✔
700

701
  // States that will update in useEffect()
702
  const [states, setStates] = useState({
33✔
703
    error: false,
33✔
704
    helperText: "",
33✔
705
    formSchema: {},
33✔
706
    validationSchema: {} as FormObject,
33✔
707
    isLoading: true,
33✔
708
  })
33✔
709
  const resolver = WizardAjvResolver(states.validationSchema, locale)
33✔
710
  const methods = useForm({ mode: "onBlur", resolver })
33✔
711

712
  const [submitting, setSubmitting] = useState(false)
33✔
713

714
  /*
715
   * Fetch json schema from either session storage or API, set schema and dereferenced version to component state.
716
   */
717
  useEffect(() => {
33✔
718
    const fetchSchema = async () => {
8✔
719
      const schema: string | null = sessionStorage.getItem(`cached_${objectType}_schema`)
8✔
720
      let parsedSchema: FormObject
8✔
721
      const ajv = new Ajv2020()
8✔
722

723
      if (!schema || !ajv.validateSchema(JSON.parse(schema))) {
8✔
724
        const response = await schemaAPIService.getSchemaByObjectType(objectType)
1✔
725
        if (response.ok) {
1!
726
          parsedSchema = response.data
×
727
          sessionStorage.setItem(`cached_${objectType}_schema`, JSON.stringify(parsedSchema))
×
728
        } else {
1✔
729
          setStates({
1✔
730
            ...states,
1✔
731
            error: true,
1✔
732
            helperText: "Unfortunately an error happened while catching form fields",
1✔
733
            isLoading: false,
1✔
734
          })
1✔
735
          return
1✔
736
        }
1✔
737
      } else {
8✔
738
        parsedSchema = JSON.parse(schema)
7✔
739
      }
7✔
740

741
      // Dereference Schema and link AccessionIds to equivalent objects
742
      let dereferencedSchema: Promise<FormObject> = await dereferenceSchema(
7✔
743
        parsedSchema as FormObject
7✔
744
      )
7✔
745

746
      dereferencedSchema = getLinkedDereferencedSchema(
7✔
747
        currentObject,
7✔
748
        parsedSchema.title.toLowerCase(),
7✔
749
        dereferencedSchema,
7✔
750
        submission.metadataObjects,
7✔
751
        analysisAccessionIds
7✔
752
      )
7✔
753

754
      setStates({
7✔
755
        ...states,
7✔
756
        formSchema: dereferencedSchema,
7✔
757
        validationSchema: parsedSchema,
7✔
758
        isLoading: false,
7✔
759
      })
7✔
760
    }
8✔
761

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

765
    // Reset current object in state on unmount
766
    return () => {
8✔
767
      dispatch(resetDraftStatus())
8✔
768
    }
8✔
769
  }, [objectType])
33✔
770

771
  // All Analysis AccessionIds
772
  const analysisAccessionIds = getAccessionIds(ObjectTypes.analysis, submission.metadataObjects)
33✔
773

774
  useEffect(() => {
33✔
775
    if (ObjectTypes.analysis) {
8✔
776
      if (analysisAccessionIds?.length > 0) {
8!
777
        // Link other Analysis AccessionIds to current Analysis form
778
        setStates(prevState => {
×
779
          return set(
×
780
            prevState,
×
781
            `formSchema.properties.analysisRef.items.properties.accessionId.enum`,
×
NEW
782
            analysisAccessionIds.filter(id => id !== currentObject?.accessionId)
×
783
          )
×
784
        })
×
785
      }
×
786
    }
8✔
787
  }, [currentObject?.accessionId, analysisAccessionIds?.length])
33✔
788

789
  /*
790
   * Submit form with cleaned values and check for response errors
791
   */
792
  const onSubmit = (data: Record<string, unknown>) => {
33✔
793
    if (Object.keys(data).length === 0) return
×
794

795
    setSubmitting(true)
×
796

797
    // Handle submitted object update
798
    const patchObject = async () => {
×
799
      const accessionId = data.accessionId as string
×
800
      const cleanedValues = JSONSchemaParser.cleanUpFormValues(data)
×
801
      try {
×
802
        const response = await objectAPIService.patchFromJSON(
×
803
          objectType,
×
804
          accessionId,
×
NEW
805
          cleanedValues
×
806
        )
×
807

808
        patchHandler(
×
809
          response,
×
810
          submission,
×
811
          currentObject.accessionId,
×
812
          objectType,
×
813
          cleanedValues,
×
NEW
814
          dispatch
×
815
        )
×
816
        dispatch(resetCurrentObject())
×
817
        methods.reset({ undefined })
×
818
        // Dispatch fileTypes if object is Run or Analysis
819
        if (objectType === ObjectTypes.run || objectType === ObjectTypes.analysis) {
×
820
          const objectWithFileTypes = getNewUniqueFileTypes(
×
821
            accessionId,
×
NEW
822
            cleanedValues as FormDataFiles
×
823
          )
×
824
          objectWithFileTypes ? dispatch(setFileTypes(objectWithFileTypes)) : null
×
825
        }
×
826
      } catch (error) {
×
827
        dispatch(
×
828
          updateStatus({
×
829
            status: ResponseStatus.error,
×
830
            response: error,
×
831
            helperText: "Unexpected error when modifying object",
×
NEW
832
          })
×
833
        )
×
834
      }
×
835

836
      setSubmitting(false)
×
837
    }
×
838

839
    // Either patch object or submit a new object
840
    if (data.status === ObjectStatus.submitted) {
×
841
      patchObject()
×
842
    } else {
×
843
      submitObjectHook(data, submission.submissionId, objectType, dispatch)
×
844
        .then(() => {
×
845
          setSubmitting(false)
×
846
          methods.reset({ undefined })
×
847
        })
×
848
        .catch(err => console.error(err))
×
849
    }
×
850
  }
×
851

852
  if (states.isLoading) return <CircularProgress />
33✔
853
  // Schema validation error differs from response status handler
854
  if (states.error)
17✔
855
    return (
17!
856
      <CustomAlert severity="error" icon={<CancelIcon sx={{ fontSize: "2rem" }} />}>
×
857
        <AlertMessage>{states.helperText}</AlertMessage>
×
858
      </CustomAlert>
✔
859
    )
860

861
  return (
17✔
862
    <>
17✔
863
      <GlobalStyles styles={{ ".MuiContainer-root": { maxWidth: "100% !important" } }} />
17✔
864
      <Container sx={{ m: 0, p: 0, width: "100%", boxSizing: "border-box" }} maxWidth={false}>
17✔
865
        <FormContent
17✔
866
          formSchema={states.formSchema as FormObject}
17✔
867
          methods={methods}
17✔
868
          onSubmit={onSubmit as SubmitHandler<FieldValues>}
17✔
869
          objectType={objectType}
17✔
870
          hasDraftObject={hasDraftObject}
17✔
871
          hasSubmittedObject={hasSubmittedObject}
17✔
872
          submission={submission}
17✔
873
          currentObject={currentObject}
17✔
874
          key={currentObject?.accessionId || submission.submissionId}
17✔
875
          closeDialog={closeDialog || (() => {})}
17✔
876
          formRef={formRef}
33✔
877
        />
878
        {submitting && <LinearProgress />}
33!
879
        <WizardXMLUploadModal
33✔
880
          open={openedXMLModal}
33✔
881
          handleClose={() => {
33✔
882
            dispatch(resetXMLModalOpen())
×
883
          }}
×
884
        />
885
      </Container>
33✔
886
    </>
33✔
887
  )
888
}
33✔
889

890
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