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

CSCfi / metadata-submitter-frontend / 17324617246

29 Aug 2025 01:06PM UTC coverage: 58.026% (+0.09%) from 57.939%
17324617246

push

github

Hang Le
Input loss warning (merge commit)

Merge branch 'feature/warn-data-loss' into 'main'
* Accommodate the lack of default values in unsaved form check

* Restore form reset on navigation for hook-form

* Refactor unsaved input checks into a hook

* Add blur on form array element removal to force force input check

* Reset unsaved status on submission exit and new object submission

* Remove resetting of form on click to navigate away

* Add datafolder link confirmation dialog

* Track form state to alert of form data loss

Closes #1089 and #1068
See merge request https://gitlab.ci.csc.fi/sds-dev/sd-submit/metadata-submitter-frontend/-/merge_requests/1156

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

594 of 809 branches covered (73.42%)

Branch coverage included in aggregate %.

165 of 251 new or added lines in 12 files covered. (65.74%)

9 existing lines in 5 files now uncovered.

5331 of 9402 relevant lines covered (56.7%)

5.32 hits per line

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

15.91
/src/components/SubmissionWizard/WizardSteps/WizardDataFolderStep.tsx
1
import React, { useState } from "react"
1✔
2

3
import HomeIcon from "@mui/icons-material/Home"
1✔
4
import NavigateNextIcon from "@mui/icons-material/NavigateNext"
1✔
5
import Box from "@mui/material/Box"
1✔
6
import Breadcrumbs from "@mui/material/Breadcrumbs"
1✔
7
import Button from "@mui/material/Button"
1✔
8
import Link from "@mui/material/Link"
1✔
9
import Typography from "@mui/material/Typography"
1✔
10
import { upperFirst } from "lodash"
1✔
11
import { useTranslation } from "react-i18next"
1✔
12

13
import { files } from "../../../../playwright/fixtures/files_response" // MOCK files array
1✔
14
import WizardStepContentHeader from "../WizardComponents/WizardStepContentHeader"
1✔
15

16
import WizardAlert from "components/SubmissionWizard/WizardComponents/WizardAlert"
1✔
17
import WizardDataFolderTable from "components/SubmissionWizard/WizardComponents/WizardDataFolderTable"
1✔
18
import WizardFilesTable from "components/SubmissionWizard/WizardComponents/WizardFilesTable"
1✔
19
import { setUnsavedForm, resetUnsavedForm } from "features/unsavedFormSlice"
1✔
20
import { addLinkedFolderToSubmission } from "features/wizardSubmissionSlice"
1✔
21
import { useAppSelector, useAppDispatch } from "hooks"
1✔
22
import { isFile } from "utils"
1✔
23

24
/*
25
 * Render folders and files from SD Connect based on user selection
26
 */
27
const WizardDataFolderStep = () => {
1✔
28
  const dispatch = useAppDispatch()
×
29
  const submission = useAppSelector(state => state.submission)
×
30
  const linkedFolder = submission.linkedFolder || ""
×
31

32
  const { t } = useTranslation()
×
33

34
  const [selectedFolder, setSelectedFolder] = useState<string>("")
×
35
  const [breadcrumbs, setBreadcrumbs] = useState<string[]>([])
×
36
  const [currentFilePath, setCurrentFilePath] = useState<string>("")
×
NEW
37
  const [alert, setAlert] = useState<boolean>(false)
×
38

NEW
39
  const handleAlert = (state: boolean) => {
×
NEW
40
    if (state) handleLinkFolder()
×
NEW
41
    setAlert(false)
×
NEW
42
  }
×
43

44
  const handleLinkFolder = async () => {
×
NEW
45
    dispatch(resetUnsavedForm())
×
46
    dispatch(addLinkedFolderToSubmission(submission.submissionId, selectedFolder))
×
47
  }
×
48

49
  const handleFolderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
×
50
    setSelectedFolder(event.target.value)
×
NEW
51
    dispatch(setUnsavedForm())
×
UNCOV
52
  }
×
53

54
  const handleFilesView = (folderName: string) => {
×
55
    const currentFolderPath = files
×
56
      .filter(file => file.path.split("/")[1] === folderName)[0]
×
57
      .path.split("/")
×
58
      .slice(0, 2)
×
59
      .join("/")
×
60
    setCurrentFilePath(currentFolderPath)
×
61
    setBreadcrumbs([t("datafolder.allFolders"), folderName])
×
62
  }
×
63

64
  const handleAddToBreadcrumbs = (subfolderName: string) => {
×
65
    setBreadcrumbs(prevState => [...prevState, subfolderName])
×
66
  }
×
67

68
  const handleClickBreadcrumb = (breadcrumb: string, index: number) => {
×
69
    /* Remove all breadcrumbs if "All folders" is clicked.
70
     * Otherwise, remove the following breadcrumbs if one breadcrumb is clicked/selected.
71
     */
72
    if (index === 0) setBreadcrumbs([])
×
73
    else setBreadcrumbs(breadcrumbs.slice(0, index + 1))
×
74

75
    /* Check if the last element of current filePath equals to selected breadcrumb,
76
     * if not, replacing current filePath with a new one that ends with the breadcrumb.
77
     */
78
    const splitFilePath = currentFilePath.split("/")
×
79
    if (breadcrumb && breadcrumb !== splitFilePath[splitFilePath.length - 1]) {
×
80
      const breadcrumbIndex = splitFilePath.indexOf(breadcrumb)
×
81
      const newFilePath = splitFilePath.slice(0, breadcrumbIndex + 1).join("/")
×
82
      setCurrentFilePath(newFilePath)
×
83
    }
×
84
  }
×
85

86
  const handleClickFileRow = (path: string, name: string) => {
×
87
    if (!isFile(files, path)) {
×
88
      /* Keep setting new filePath if current filePath's length < original filePath's length.
89
       * It means that the current file is still nested under subfolder
90
       */
91
      setCurrentFilePath(path)
×
92
      handleAddToBreadcrumbs(name)
×
93
    }
×
94
  }
×
95

96
  const linkFolderButton = (
×
97
    <Button
×
98
      disabled={!selectedFolder || !!linkedFolder}
×
99
      variant="contained"
×
100
      aria-label={t("ariaLabels.linkFolder")}
×
101
      size="small"
×
102
      type="submit"
×
NEW
103
      onClick={() => setAlert(true)}
×
104
      data-testid="link-datafolder"
×
105
    >
106
      {t("datafolder.linkFolder")}
×
107
    </Button>
×
108
  )
109

110
  const renderHeading = () => (
×
111
    <Typography variant="h5" fontWeight="700" color="secondary">
×
112
      {linkedFolder ? t("datafolder.linkedFolder") : t("datafolder.linkFromSDConnect")}
×
113
    </Typography>
×
114
  )
115

116
  const renderBreadcrumbs = () =>
×
117
    !!breadcrumbs.length && (
×
118
      <Breadcrumbs
×
119
        separator={<NavigateNextIcon fontSize="large" />}
×
120
        aria-label={t("ariaLabels.folderBreadcrumb")}
×
121
        data-testid="folder-breadcrumb"
×
122
      >
123
        {breadcrumbs.map((el, index) => (
×
124
          <Link
×
125
            key={index}
×
126
            underline="none"
×
127
            color="primary"
×
128
            href="#"
×
129
            onClick={() => handleClickBreadcrumb(el, index)}
×
130
            data-testid={el}
×
131
          >
132
            {index === 0 && (
×
133
              <HomeIcon
×
134
                color="primary"
×
135
                fontSize="large"
×
136
                sx={{ mr: "0.5rem", verticalAlign: "middle" }}
×
137
              />
138
            )}
139
            {index === 0 ? t("datafolder.allFolders") : upperFirst(el)}
×
140
          </Link>
×
141
        ))}
×
142
      </Breadcrumbs>
×
143
    )
144

145
  const renderFolderTable = () =>
×
146
    !breadcrumbs.length && (
×
147
      <WizardDataFolderTable
×
148
        selectedFolder={selectedFolder}
×
149
        linkedFolder={linkedFolder}
×
150
        handleFolderChange={handleFolderChange}
×
151
        handleFilesView={handleFilesView}
×
152
      />
153
    )
154

155
  const renderFileTable = () =>
×
156
    !!breadcrumbs.length && (
×
157
      <WizardFilesTable
×
158
        currentFilePath={currentFilePath}
×
159
        files={files}
×
160
        handleClickFileRow={handleClickFileRow}
×
161
      />
162
    )
163

164
  return (
×
165
    <Box>
×
166
      <WizardStepContentHeader action={linkFolderButton} />
×
167
      <Box display="flex" flexDirection="column" p="5rem" gap={4}>
×
168
        {renderHeading()}
×
169
        {renderBreadcrumbs()}
×
170
        {renderFolderTable()}
×
171
        {renderFileTable()}
×
172
      </Box>
×
NEW
173
      {alert && <WizardAlert onAlert={handleAlert} parentLocation="submission" alertType="link" />}
×
UNCOV
174
    </Box>
×
175
  )
176
}
×
177

178
export default WizardDataFolderStep
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