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

CSCfi / metadata-submitter-frontend / 15155733929

21 May 2025 07:05AM UTC coverage: 49.155% (+1.3%) from 47.832%
15155733929

push

github

mradavi
Remove template-related functionality (merge commit)

Merge branch 'feature/remove-templates' into 'main'
* Remove template-related functionality

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

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

647 of 948 branches covered (68.25%)

Branch coverage included in aggregate %.

6 of 8 new or added lines in 4 files covered. (75.0%)

24 existing lines in 4 files now uncovered.

6160 of 12900 relevant lines covered (47.75%)

4.45 hits per line

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

57.75
/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 { setCurrentObject, resetCurrentObject } from "features/wizardCurrentObjectSlice"
1✔
40
import {
41
  deleteObjectFromSubmission,
42
  replaceObjectInSubmission,
43
  addDoiInfoToSubmission,
44
} from "features/wizardSubmissionSlice"
1✔
45
import { setXMLModalOpen, resetXMLModalOpen } from "features/wizardXMLModalSlice"
1✔
46
import { useAppSelector, useAppDispatch } from "hooks"
1✔
47
import objectAPIService from "services/objectAPI"
1✔
48
import schemaAPIService from "services/schemaAPI"
1✔
49
import type {
50
  DoiFormDetails,
51
  SubmissionDetailsWithId,
52
  FormDataFiles,
53
  FormObject,
54
  ObjectDetails,
55
  ObjectDisplayValues,
56
  FormRef,
57
} from "types"
58
import {
59
  getObjectDisplayTitle,
60
  getAccessionIds,
61
  getNewUniqueFileTypes,
62
  checkObjectStatus,
63
} from "utils"
1✔
64
import { dereferenceSchema } from "utils/JSONSchemaUtils"
1✔
65

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

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

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

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

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

150
type FormContentProps = {
151
  methods: UseFormReturn
152
  formSchema: FormObject
153
  onSubmit: SubmitHandler<FieldValues>
154
  objectType: string
155
  submission: SubmissionDetailsWithId
156
  hasDraftObject: boolean
157
  hasSubmittedObject: boolean
158
  currentObject: ObjectDetails & { objectId: string; [key: string]: unknown }
159
  formRef?: FormRef
160
}
161

162
/*
163
 * Create header for form card with button to close the card
164
 */
165
const CustomCardHeader = (props: CustomCardHeaderProps) => {
1✔
166
  const {
16✔
167
    hasSubmittedObject,
16✔
168
    hasDraftObject,
16✔
169
    objectType,
16✔
170
    currentObject,
16✔
171
    refForm,
16✔
172
    onClickSaveDraft,
16✔
173
    onClickSubmit,
16✔
174
    onClickSaveDOI,
16✔
175
    onClickClearForm,
16✔
176
    onOpenXMLModal,
16✔
177
    onDeleteForm,
16✔
178
  } = props
16✔
179

180
  const focusTarget = useRef<HTMLButtonElement>(null)
16✔
181
  const shouldFocus = useAppSelector(state => state.focus)
16✔
182
  const { t } = useTranslation()
16✔
183

184
  useEffect(() => {
16✔
185
    if (shouldFocus && focusTarget.current) focusTarget.current.focus()
7!
186
  }, [shouldFocus])
16✔
187

188
  const buttonGroup = (
16✔
189
    <Box display="flex">
16✔
190
      <WizardOptions
16✔
191
        objectType={objectType}
16✔
192
        onClearForm={onClickClearForm}
16✔
193
        onOpenXMLModal={onOpenXMLModal}
16✔
194
        onDeleteForm={onDeleteForm}
16✔
195
        disableUploadXML={
16✔
196
          objectType === ObjectTypes.study && (hasDraftObject || hasSubmittedObject)
16✔
197
        }
16✔
198
      />
199
      <ButtonGroup>
16✔
200
        <Button
16✔
201
          variant="contained"
16✔
202
          aria-label="save form as draft"
16✔
203
          size="small"
16✔
204
          onClick={onClickSaveDraft}
16✔
205
          data-testid="form-draft"
16✔
206
          disabled={objectType === ObjectTypes.study && hasSubmittedObject}
16✔
207
        >
208
          {(objectType === ObjectTypes.study && hasDraftObject) ||
16✔
209
          currentObject?.status === ObjectStatus.draft
16!
210
            ? t("formActions.updateDraft")
×
211
            : t("formActions.saveAsDraft")}
16✔
212
        </Button>
16✔
213
        <Button
16✔
214
          variant="contained"
16✔
215
          aria-label="submit form"
16✔
216
          size="small"
16✔
217
          type="submit"
16✔
218
          onClick={onClickSubmit}
16✔
219
          form={refForm}
16✔
220
          data-testid="form-ready"
16✔
221
          disabled={
16✔
222
            objectType === ObjectTypes.study && hasSubmittedObject && !currentObject.accessionId
16✔
223
          }
224
        >
225
          {(objectType === ObjectTypes.study && hasSubmittedObject) ||
16✔
226
          currentObject?.status === ObjectStatus.submitted
5✔
227
            ? t("formActions.update")
11✔
228
            : t("formActions.markAsReady")}
5✔
229
        </Button>
16✔
230
      </ButtonGroup>
16✔
231
    </Box>
16✔
232
  )
233

234
  const doiButtonGroup = (
16✔
235
    <Box display="flex">
16✔
236
      <WizardOptions
16✔
237
        objectType={objectType}
16✔
238
        onClearForm={onClickClearForm}
16✔
239
        onOpenXMLModal={onOpenXMLModal}
16✔
240
        onDeleteForm={onDeleteForm}
16✔
241
      />
242
      <ButtonGroup>
16✔
243
        <Button
16✔
244
          variant="contained"
16✔
245
          aria-label="save Datacite"
16✔
246
          size="small"
16✔
247
          onClick={onClickSaveDOI}
16✔
248
          data-testid="form-datacite"
16✔
249
        >
250
          {t("save")}
16✔
251
        </Button>
16✔
252
      </ButtonGroup>
16✔
253
    </Box>
16✔
254
  )
255

256
  return (
16✔
257
    <WizardStepContentHeader
16✔
258
      action={objectType === ObjectTypes.datacite ? doiButtonGroup : buttonGroup}
16!
259
    />
260
  )
261
}
16✔
262

263
/*
264
 * Draft save and object patch use both same response handler
265
 */
266
const patchHandler = (
1✔
267
  response: ApiResponse<unknown>,
×
268
  submission: SubmissionDetailsWithId,
×
269
  accessionId: string,
×
270
  objectType: string,
×
271
  cleanedValues: Record<string, unknown>,
×
272
  dispatch: (reducer: unknown) => void
×
273
) => {
×
274
  if (response.ok) {
×
275
    dispatch(
×
276
      replaceObjectInSubmission(
×
277
        accessionId,
×
278
        {
×
279
          submissionType: ObjectSubmissionTypes.form,
×
280
          displayTitle: getObjectDisplayTitle(objectType, cleanedValues as ObjectDisplayValues),
×
281
        },
×
282
        ObjectStatus.submitted
×
283
      )
×
284
    )
×
285
    dispatch(resetDraftStatus())
×
286
    dispatch(
×
287
      updateStatus({
×
288
        status: ResponseStatus.success,
×
289
        response: response,
×
290
        helperText: "",
×
291
      })
×
292
    )
×
293
  } else {
×
294
    dispatch(
×
295
      updateStatus({
×
296
        status: ResponseStatus.error,
×
297
        response: response,
×
298
        helperText: "Unexpected error",
×
299
      })
×
300
    )
×
301
  }
×
302
}
×
303

304
/*
305
 * Return react-hook-form based form which is rendered from schema and checked against resolver. Set default values when continuing draft
306
 */
307
const FormContent = ({
1✔
308
  methods,
16✔
309
  formSchema,
16✔
310
  onSubmit,
16✔
311
  objectType,
16✔
312
  hasDraftObject,
16✔
313
  hasSubmittedObject,
16✔
314
  submission,
16✔
315
  currentObject,
16✔
316
  formRef,
16✔
317
}: FormContentProps) => {
16✔
318
  const dispatch = useAppDispatch()
16✔
319

320
  const alert = useAppSelector(state => state.alert)
16✔
321
  const clearForm = useAppSelector(state => state.clearForm)
16✔
322

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

326
  const autoSaveTimer: { current: NodeJS.Timeout | null } = useRef(null)
16✔
327
  let timer = 0
16✔
328

329
  // Set form default values
330
  useEffect(() => {
16✔
331
    try {
7✔
332
      const mutable = cloneDeep(currentObject)
7✔
333
      methods.reset(mutable)
7✔
334
    } catch (e) {
7!
335
      console.error("Reset failed:", e)
×
336
    }
×
337
  }, [currentObject?.accessionId])
16✔
338

339
  useEffect(() => {
16✔
340
    dispatch(setClearForm(false))
7✔
341
  }, [clearForm])
16✔
342

343
  // Check if form has been edited
344
  useEffect(() => {
16✔
345
    checkDirty()
8✔
346
  }, [methods.formState.isDirty])
16✔
347

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

350
  useEffect(() => {
16✔
351
    // Delete draft form ONLY if the form was successfully submitted
352
    if (isSubmitSuccessful) {
7!
353
      if (
×
354
        currentObject?.status === ObjectStatus.draft &&
×
355
        currentObjectId &&
×
356
        Object.keys(currentObject).length > 0
×
357
      ) {
×
358
        handleDeleteForm()
×
359
      }
×
360
    }
×
361
  }, [isSubmitSuccessful])
16✔
362

363
  const handleClearForm = () => {
16✔
364
    resetTimer()
×
365
    methods.reset({ undefined })
×
366
    dispatch(setClearForm(true))
×
367
    dispatch(
×
368
      setCurrentObject({
×
369
        objectId: currentObjectId,
×
370
        status: currentObject.status,
×
371
      })
×
372
    )
×
373
  }
×
374

375
  // Check if the form is empty
376
  const isFormCleanedValuesEmpty = (cleanedValues: {
16✔
377
    [x: string]: unknown
378
    [x: number]: unknown
379
    accessionId?: string
380
    lastModified?: string
381
    objectType?: string
382
    status?: string
383
    title?: string
384
    submissionType?: string
385
  }) => {
2✔
386
    return Object.keys(cleanedValues).filter(val => val !== "index").length === 0
2✔
387
  }
2✔
388

389
  const checkDirty = () => {
16✔
390
    const isFormTouched = () => {
10✔
391
      return Object.keys(methods.formState.dirtyFields).length > 0
10✔
392
    }
10✔
393

394
    if (isFormTouched()) {
10✔
395
      dispatch(setDraftStatus("notSaved"))
2✔
396
    } else dispatch(resetDraftStatus())
10✔
397
  }
10✔
398

399
  const getCleanedValues = () =>
16✔
400
    JSONSchemaParser.cleanUpFormValues(methods.getValues()) as ObjectDetails
2✔
401

402
  // Draft data is set to state on every change to form
403
  const handleChange = () => {
16✔
404
    clearForm ? dispatch(setClearForm(false)) : null
2!
405
    const clone = cloneDeep(currentObject)
2✔
406
    const values = getCleanedValues()
2✔
407

408
    if (clone && !isFormCleanedValuesEmpty(values)) {
2✔
409
      Object.keys(values).forEach(item => (clone[item] = values[item]))
2✔
410

411
      !currentObject.accessionId && currentObjectId
2!
412
        ? dispatch(
×
413
            setCurrentObject({
×
414
              ...clone,
×
415
              cleanedValues: values,
×
416
              status: currentObject.status || ObjectStatus.draft,
×
417
              objectId: currentObjectId,
×
418
            })
×
419
          )
×
420
        : dispatch(setCurrentObject({ ...clone, cleanedValues: values }))
2✔
421
      checkDirty()
2✔
422
    } else {
2!
423
      dispatch(resetDraftStatus())
×
424
      resetTimer()
×
425
    }
×
426
  }
2✔
427

428
  const handleDOISubmit = async (data: DoiFormDetails) => {
16✔
429
    dispatch(addDoiInfoToSubmission(submission.submissionId, data))
×
430
      .then(() => {
×
431
        dispatch(resetAutocompleteField())
×
432
        dispatch(resetCurrentObject())
×
433
        dispatch(
×
434
          updateStatus({
×
435
            status: ResponseStatus.success,
×
436
            helperText: "snackbarMessages.success.doi.saved",
×
437
          })
×
438
        )
×
439
      })
×
440
      .catch(error =>
×
441
        dispatch(
×
442
          updateStatus({
×
443
            status: ResponseStatus.error,
×
444
            response: error,
×
445
            helperText: "snackbarMessages.error.helperText.submitDoiError",
×
446
          })
×
447
        )
×
448
      )
×
449
  }
×
450

451
  /*
452
   * Logic for auto-save feature.
453
   * We use setDraftAutoSaveAllowed state change to render form before save.
454
   * This helps with getting current accession ID and form data without rendering on every timer increment.
455
   */
456
  const startTimer = () => {
16✔
457
    autoSaveTimer.current = setInterval(() => {
2✔
458
      timer = timer + 1
×
459
      if (timer >= 60) {
×
460
        setDraftAutoSaveAllowed(true)
×
461
      }
×
462
    }, 1000)
2✔
463
  }
2✔
464

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

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

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

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

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

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

495
  const emptyFormError = () => {
16✔
496
    dispatch(
×
497
      updateStatus({
×
498
        status: ResponseStatus.info,
×
499
        helperText: "An empty form cannot be saved. Please fill in the form before saving it.",
×
500
      })
×
501
    )
×
502
  }
×
503

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

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

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

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

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

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

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

573
  return (
16✔
574
    <FormProvider {...methods}>
16✔
575
      <CustomCardHeader
16✔
576
        hasSubmittedObject={hasSubmittedObject}
16✔
577
        hasDraftObject={hasDraftObject}
16✔
578
        objectType={objectType}
16✔
579
        currentObject={currentObject}
16✔
580
        refForm="hook-form"
16✔
581
        onClickSaveDraft={() => handleSaveDraft()}
16✔
582
        onClickSubmit={() => resetTimer()}
16✔
583
        onClickSaveDOI={methods.handleSubmit(async data => handleDOISubmit(data as DoiFormDetails))}
16✔
584
        onClickClearForm={() => handleClearForm()}
16✔
585
        onOpenXMLModal={() => handleXMLModalOpen()}
16✔
586
        onDeleteForm={() => handleDeleteForm()}
16✔
587
      />
588

589
      <Form
16✔
590
        id="hook-form"
16✔
591
        onChange={() => handleChange()}
16✔
592
        onSubmit={methods.handleSubmit(onSubmit)}
16✔
593
        ref={formRef as RefObject<HTMLFormElement>}
16✔
594
        onReset={handleReset}
16✔
595
      >
596
        <Box>{JSONSchemaParser.buildFields(formSchema)}</Box>
16✔
597
      </Form>
16✔
598
    </FormProvider>
16✔
599
  )
600
}
16✔
601

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

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

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

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

628
  const [submitting, setSubmitting] = useState(false)
33✔
629

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

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

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

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

670
      setStates({
7✔
671
        ...states,
7✔
672
        formSchema: dereferencedSchema,
7✔
673
        validationSchema: parsedSchema,
7✔
674
        isLoading: false,
7✔
675
      })
7✔
676
    }
8✔
677

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

681
    // Reset current object in state on unmount
682
    return () => {
8✔
683
      dispatch(resetDraftStatus())
8✔
684
    }
8✔
685
  }, [objectType])
33✔
686

687
  // All Analysis AccessionIds
688
  const analysisAccessionIds = getAccessionIds(ObjectTypes.analysis, submission.metadataObjects)
33✔
689

690
  useEffect(() => {
33✔
691
    if (ObjectTypes.analysis) {
8✔
692
      if (analysisAccessionIds?.length > 0) {
8!
693
        // Link other Analysis AccessionIds to current Analysis form
694
        setStates(prevState => {
×
695
          return set(
×
696
            prevState,
×
697
            `formSchema.properties.analysisRef.items.properties.accessionId.enum`,
×
698
            analysisAccessionIds.filter(id => id !== currentObject?.accessionId)
×
699
          )
×
700
        })
×
701
      }
×
702
    }
8✔
703
  }, [currentObject?.accessionId, analysisAccessionIds?.length])
33✔
704

705
  /*
706
   * Submit form with cleaned values and check for response errors
707
   */
708
  const onSubmit = (data: Record<string, unknown>) => {
33✔
709
    if (Object.keys(data).length === 0) return
×
710

711
    setSubmitting(true)
×
712

713
    // Handle submitted object update
714
    const patchObject = async () => {
×
715
      const accessionId = data.accessionId as string
×
716
      const cleanedValues = JSONSchemaParser.cleanUpFormValues(data)
×
717
      try {
×
718
        const response = await objectAPIService.patchFromJSON(
×
719
          objectType,
×
720
          accessionId,
×
721
          cleanedValues
×
722
        )
×
723

724
        patchHandler(
×
725
          response,
×
726
          submission,
×
727
          currentObject.accessionId,
×
728
          objectType,
×
729
          cleanedValues,
×
730
          dispatch
×
731
        )
×
732
        dispatch(resetCurrentObject())
×
733
        methods.reset({ undefined })
×
734
        // Dispatch fileTypes if object is Run or Analysis
735
        if (objectType === ObjectTypes.run || objectType === ObjectTypes.analysis) {
×
736
          const objectWithFileTypes = getNewUniqueFileTypes(
×
737
            accessionId,
×
738
            cleanedValues as FormDataFiles
×
739
          )
×
740
          objectWithFileTypes ? dispatch(setFileTypes(objectWithFileTypes)) : null
×
741
        }
×
742
      } catch (error) {
×
743
        dispatch(
×
744
          updateStatus({
×
745
            status: ResponseStatus.error,
×
746
            response: error,
×
747
            helperText: "Unexpected error when modifying object",
×
748
          })
×
749
        )
×
750
      }
×
751

752
      setSubmitting(false)
×
753
    }
×
754

755
    // Either patch object or submit a new object
756
    if (data.status === ObjectStatus.submitted) {
×
757
      patchObject()
×
758
    } else {
×
759
      submitObjectHook(data, submission.submissionId, objectType, dispatch)
×
760
        .then(() => {
×
761
          setSubmitting(false)
×
762
          methods.reset({ undefined })
×
763
        })
×
764
        .catch(err => console.error(err))
×
765
    }
×
766
  }
×
767

768
  if (states.isLoading) return <CircularProgress />
33✔
769
  // Schema validation error differs from response status handler
770
  if (states.error)
17✔
771
    return (
17!
772
      <CustomAlert severity="error" icon={<CancelIcon sx={{ fontSize: "2rem" }} />}>
×
773
        <AlertMessage>{states.helperText}</AlertMessage>
×
774
      </CustomAlert>
✔
775
    )
776

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

805
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