@openneuro/app 4.35.0 → 4.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/client.jsx +1 -0
- package/src/scripts/components/button/button.scss +14 -0
- package/src/scripts/components/page/page.scss +2 -70
- package/src/scripts/components/search-page/SearchResultItem.tsx +3 -1
- package/src/scripts/components/search-page/search-page.scss +1 -13
- package/src/scripts/config.ts +6 -0
- package/src/scripts/dataset/files/__tests__/__snapshots__/file.spec.jsx.snap +2 -2
- package/src/scripts/dataset/files/file.tsx +2 -2
- package/src/scripts/dataset/mutations/__tests__/update-file.spec.tsx +126 -0
- package/src/scripts/dataset/mutations/update-file.jsx +20 -5
- package/src/scripts/dataset/routes/snapshot.tsx +1 -1
- package/src/scripts/errors/errorRoute.tsx +2 -0
- package/src/scripts/pages/admin/__tests__/users.spec.tsx +51 -18
- package/src/scripts/pages/admin/admin.jsx +2 -2
- package/src/scripts/pages/admin/user-fragment.ts +10 -3
- package/src/scripts/pages/admin/user-summary.tsx +100 -0
- package/src/scripts/pages/admin/user-tools.tsx +81 -58
- package/src/scripts/pages/admin/users.module.scss +277 -0
- package/src/scripts/pages/admin/users.tsx +351 -152
- package/src/scripts/queries/user.ts +120 -3
- package/src/scripts/queries/users.ts +247 -0
- package/src/scripts/routes.tsx +7 -15
- package/src/scripts/types/user-types.ts +12 -13
- package/src/scripts/uploader/file-select.tsx +42 -57
- package/src/scripts/uploader/upload-select.jsx +1 -1
- package/src/scripts/users/__tests__/dataset-card.spec.tsx +127 -0
- package/src/scripts/users/__tests__/user-account-view.spec.tsx +150 -67
- package/src/scripts/users/__tests__/user-card.spec.tsx +6 -17
- package/src/scripts/users/__tests__/user-query.spec.tsx +133 -38
- package/src/scripts/users/__tests__/user-routes.spec.tsx +156 -27
- package/src/scripts/users/__tests__/user-tabs.spec.tsx +7 -7
- package/src/scripts/users/components/edit-list.tsx +26 -5
- package/src/scripts/users/components/edit-string.tsx +40 -13
- package/src/scripts/users/components/editable-content.tsx +10 -3
- package/src/scripts/users/components/user-dataset-filters.tsx +205 -121
- package/src/scripts/users/dataset-card.tsx +3 -2
- package/src/scripts/users/github-auth-button.tsx +98 -0
- package/src/scripts/users/scss/datasetcard.module.scss +65 -12
- package/src/scripts/users/scss/useraccountview.module.scss +1 -1
- package/src/scripts/users/user-account-view.tsx +43 -34
- package/src/scripts/users/user-card.tsx +23 -22
- package/src/scripts/users/user-container.tsx +9 -5
- package/src/scripts/users/user-datasets-view.tsx +350 -40
- package/src/scripts/users/user-menu.tsx +4 -9
- package/src/scripts/users/user-notifications-view.tsx +9 -7
- package/src/scripts/users/user-query.tsx +3 -6
- package/src/scripts/users/user-routes.tsx +11 -5
- package/src/scripts/users/user-tabs.tsx +4 -2
- package/src/scripts/users/__tests__/datasest-card.spec.tsx +0 -201
- package/src/scripts/users/fragments/query.js +0 -42
|
@@ -1,11 +1,86 @@
|
|
|
1
|
+
import { vi } from "vitest"
|
|
1
2
|
import React from "react"
|
|
2
3
|
import { cleanup, render, screen } from "@testing-library/react"
|
|
3
4
|
import { MemoryRouter } from "react-router-dom"
|
|
4
5
|
import { MockedProvider } from "@apollo/client/testing"
|
|
6
|
+
|
|
7
|
+
// Component under test
|
|
5
8
|
import { UserRoutes } from "../user-routes"
|
|
9
|
+
|
|
10
|
+
// Types and Queries
|
|
6
11
|
import type { User } from "../../types/user-types"
|
|
7
|
-
import {
|
|
8
|
-
|
|
12
|
+
import { ADVANCED_SEARCH_DATASETS_QUERY, GET_USER } from "../../queries/user"
|
|
13
|
+
|
|
14
|
+
vi.mock("./user-container", () => {
|
|
15
|
+
return {
|
|
16
|
+
UserAccountContainer: vi.fn((props) => (
|
|
17
|
+
<div data-testid="mock-user-account-container">
|
|
18
|
+
Mocked UserAccountContainer
|
|
19
|
+
{props.children}
|
|
20
|
+
<p>Container ORCID: {props.orcidUser?.orcid}</p>
|
|
21
|
+
<p>Container Has Edit: {props.hasEdit ? "true" : "false"}</p>
|
|
22
|
+
<p>Container Is User: {props.isUser ? "true" : "false"}</p>
|
|
23
|
+
</div>
|
|
24
|
+
)),
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
vi.mock("./user-account-view", () => ({
|
|
29
|
+
UserAccountView: vi.fn((props) => (
|
|
30
|
+
<div data-testid="user-account-view">
|
|
31
|
+
Mocked UserAccountView
|
|
32
|
+
<p>View ORCID: {props.orcidUser?.orcid}</p>
|
|
33
|
+
</div>
|
|
34
|
+
)),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
vi.mock("./user-notifications-view", () => ({
|
|
38
|
+
UserNotificationsView: vi.fn((props) => (
|
|
39
|
+
<div data-testid="user-notifications-view">
|
|
40
|
+
Mocked UserNotificationsView
|
|
41
|
+
{props.children}
|
|
42
|
+
<p>Notifications ORCID: {props.orcidUser?.orcid}</p>
|
|
43
|
+
</div>
|
|
44
|
+
)),
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
vi.mock("./user-datasets-view", () => ({
|
|
48
|
+
UserDatasetsView: vi.fn((props) => (
|
|
49
|
+
<div data-testid="user-datasets-view">
|
|
50
|
+
Mocked UserDatasetsView
|
|
51
|
+
<p>Datasets ORCID: {props.orcidUser?.orcid}</p>
|
|
52
|
+
<p>Datasets Has Edit: {props.hasEdit ? "true" : "false"}</p>
|
|
53
|
+
</div>
|
|
54
|
+
)),
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
vi.mock("../errors/404page", () => ({
|
|
58
|
+
default: vi.fn(() => (
|
|
59
|
+
<div data-testid="404-page">
|
|
60
|
+
404: The page you are looking for does not exist.
|
|
61
|
+
</div>
|
|
62
|
+
)),
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
vi.mock("../errors/403page", () => ({
|
|
66
|
+
default: vi.fn(() => (
|
|
67
|
+
<div data-testid="403-page">
|
|
68
|
+
403: You do not have access to this page, you may need to sign in.
|
|
69
|
+
</div>
|
|
70
|
+
)),
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
vi.mock("./user-notifications-tab-content", () => ({
|
|
74
|
+
UnreadNotifications: vi.fn(() => (
|
|
75
|
+
<div data-testid="unread-notifications">Unread Notifications</div>
|
|
76
|
+
)),
|
|
77
|
+
SavedNotifications: vi.fn(() => (
|
|
78
|
+
<div data-testid="saved-notifications">Saved Notifications</div>
|
|
79
|
+
)),
|
|
80
|
+
ArchivedNotifications: vi.fn(() => (
|
|
81
|
+
<div data-testid="archived-notifications">Archived Notifications</div>
|
|
82
|
+
)),
|
|
83
|
+
}))
|
|
9
84
|
|
|
10
85
|
const defaultUser: User = {
|
|
11
86
|
id: "1",
|
|
@@ -15,15 +90,33 @@ const defaultUser: User = {
|
|
|
15
90
|
institution: "Unknown Institution",
|
|
16
91
|
email: "john.doe@example.com",
|
|
17
92
|
avatar: "https://dummyimage.com/200x200/000/fff",
|
|
18
|
-
orcid: "0000-0000-0000-0000",
|
|
93
|
+
orcid: "0000-0000-0000-0000", // Ensure ORCID is present for mocks
|
|
19
94
|
links: [],
|
|
20
95
|
}
|
|
21
96
|
|
|
22
97
|
const mocks = [
|
|
23
98
|
{
|
|
24
99
|
request: {
|
|
25
|
-
query:
|
|
26
|
-
variables: {
|
|
100
|
+
query: ADVANCED_SEARCH_DATASETS_QUERY,
|
|
101
|
+
variables: {
|
|
102
|
+
first: 26,
|
|
103
|
+
query: {
|
|
104
|
+
bool: {
|
|
105
|
+
filter: [
|
|
106
|
+
{
|
|
107
|
+
terms: {
|
|
108
|
+
"permissions.userPermissions.user.id": [defaultUser.id],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
must: [{ match_all: {} }],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
sortBy: null,
|
|
116
|
+
cursor: null,
|
|
117
|
+
allDatasets: true,
|
|
118
|
+
datasetStatus: undefined,
|
|
119
|
+
},
|
|
27
120
|
},
|
|
28
121
|
result: {
|
|
29
122
|
data: {
|
|
@@ -72,7 +165,7 @@ const mocks = [
|
|
|
72
165
|
{
|
|
73
166
|
request: {
|
|
74
167
|
query: GET_USER,
|
|
75
|
-
variables: {
|
|
168
|
+
variables: { id: defaultUser.orcid },
|
|
76
169
|
},
|
|
77
170
|
result: {
|
|
78
171
|
data: {
|
|
@@ -81,9 +174,8 @@ const mocks = [
|
|
|
81
174
|
},
|
|
82
175
|
},
|
|
83
176
|
]
|
|
84
|
-
|
|
85
177
|
const renderWithRouter = (
|
|
86
|
-
|
|
178
|
+
orcidUser: User,
|
|
87
179
|
route: string,
|
|
88
180
|
hasEdit: boolean,
|
|
89
181
|
isUser: boolean,
|
|
@@ -91,53 +183,90 @@ const renderWithRouter = (
|
|
|
91
183
|
return render(
|
|
92
184
|
<MockedProvider mocks={mocks} addTypename={false}>
|
|
93
185
|
<MemoryRouter initialEntries={[route]}>
|
|
94
|
-
<UserRoutes
|
|
186
|
+
<UserRoutes orcidUser={orcidUser} hasEdit={hasEdit} isUser={isUser} />
|
|
95
187
|
</MemoryRouter>
|
|
96
188
|
</MockedProvider>,
|
|
97
189
|
)
|
|
98
190
|
}
|
|
99
191
|
|
|
100
192
|
describe("UserRoutes Component", () => {
|
|
101
|
-
const
|
|
193
|
+
const userToPass: User = defaultUser
|
|
194
|
+
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
cleanup()
|
|
197
|
+
vi.clearAllMocks()
|
|
198
|
+
vi.resetAllMocks()
|
|
199
|
+
})
|
|
102
200
|
|
|
103
201
|
it("renders UserDatasetsView for the default route", async () => {
|
|
104
|
-
renderWithRouter(
|
|
202
|
+
renderWithRouter(userToPass, "/", true, true)
|
|
203
|
+
// Expect the default to be the datasets view
|
|
105
204
|
const datasetsView = await screen.findByTestId("user-datasets-view")
|
|
106
205
|
expect(datasetsView).toBeInTheDocument()
|
|
206
|
+
expect(screen.getByText(userToPass.orcid)).toBeInTheDocument()
|
|
107
207
|
})
|
|
108
208
|
|
|
109
209
|
it("renders FourOFourPage for an invalid route", async () => {
|
|
110
|
-
renderWithRouter(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
210
|
+
renderWithRouter(userToPass, "/nonexistent-route", true, true)
|
|
211
|
+
// Expect the mocked 404 page
|
|
212
|
+
expect(
|
|
213
|
+
screen.getByText(/404: The page you are looking for does not exist./i),
|
|
214
|
+
).toBeInTheDocument()
|
|
115
215
|
})
|
|
116
216
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
217
|
+
it("renders UserAccountView when hasEdit is true", async () => {
|
|
218
|
+
renderWithRouter(userToPass, "/account", true, true)
|
|
219
|
+
// Expect the mocked account view
|
|
220
|
+
const accountView = await screen.findByTestId("user-account-view")
|
|
221
|
+
expect(accountView).toBeInTheDocument()
|
|
222
|
+
})
|
|
122
223
|
|
|
123
224
|
it("renders UserNotificationsView when hasEdit is true", async () => {
|
|
124
|
-
renderWithRouter(
|
|
225
|
+
renderWithRouter(userToPass, "/notifications", true, true)
|
|
226
|
+
// Expect the mocked notifications view
|
|
227
|
+
const notificationsView = await screen.findByTestId(
|
|
228
|
+
"user-notifications-view",
|
|
229
|
+
)
|
|
230
|
+
expect(notificationsView).toBeInTheDocument()
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it("renders SavedNotifications within UserNotificationsView", async () => {
|
|
234
|
+
renderWithRouter(userToPass, "/notifications/saved", true, true)
|
|
125
235
|
const notificationsView = await screen.findByTestId(
|
|
126
236
|
"user-notifications-view",
|
|
127
237
|
)
|
|
128
238
|
expect(notificationsView).toBeInTheDocument()
|
|
239
|
+
expect(screen.getByText("Saved Notification Example")).toBeInTheDocument()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it("renders ArchivedNotifications within UserNotificationsView", async () => {
|
|
243
|
+
renderWithRouter(userToPass, "/notifications/archived", true, true)
|
|
244
|
+
const notificationsView = await screen.findByTestId(
|
|
245
|
+
"user-notifications-view",
|
|
246
|
+
)
|
|
247
|
+
expect(notificationsView).toBeInTheDocument()
|
|
248
|
+
expect(screen.getByText("Archived Notification Example"))
|
|
249
|
+
.toBeInTheDocument()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("renders 404 for unknown notification sub-route", async () => {
|
|
253
|
+
renderWithRouter(userToPass, "/notifications/nonexistent", true, true)
|
|
254
|
+
expect(
|
|
255
|
+
screen.getByText(/404: The page you are looking for does not exist./i),
|
|
256
|
+
).toBeInTheDocument()
|
|
129
257
|
})
|
|
130
258
|
|
|
131
259
|
it("renders FourOThreePage when hasEdit is false for restricted routes", async () => {
|
|
132
260
|
const restrictedRoutes = ["/account", "/notifications"]
|
|
133
261
|
|
|
134
262
|
for (const route of restrictedRoutes) {
|
|
263
|
+
renderWithRouter(userToPass, route, false, true)
|
|
264
|
+
expect(
|
|
265
|
+
screen.getByText(
|
|
266
|
+
/403: You do not have access to this page, you may need to sign in./i,
|
|
267
|
+
),
|
|
268
|
+
).toBeInTheDocument()
|
|
135
269
|
cleanup()
|
|
136
|
-
renderWithRouter(user, route, false, true)
|
|
137
|
-
const errorMessage = await screen.findByText(
|
|
138
|
-
/403: You do not have access to this page, you may need to sign in./i,
|
|
139
|
-
)
|
|
140
|
-
expect(errorMessage).toBeInTheDocument()
|
|
141
270
|
}
|
|
142
271
|
})
|
|
143
272
|
})
|
|
@@ -54,12 +54,11 @@ describe("UserAccountTabs Component", () => {
|
|
|
54
54
|
const datasetsTab = screen.getByText("My Datasets")
|
|
55
55
|
expect(hasActiveClass(datasetsTab)).toBe(true)
|
|
56
56
|
|
|
57
|
-
const notificationsTab = screen.getByText("Notifications")
|
|
57
|
+
//const notificationsTab = screen.getByText("Notifications")
|
|
58
58
|
|
|
59
|
-
fireEvent.click(notificationsTab)
|
|
59
|
+
//fireEvent.click(notificationsTab)
|
|
60
60
|
|
|
61
|
-
expect(hasActiveClass(notificationsTab)).toBe(true)
|
|
62
|
-
expect(hasActiveClass(datasetsTab)).toBe(false)
|
|
61
|
+
//expect(hasActiveClass(notificationsTab)).toBe(true)
|
|
63
62
|
|
|
64
63
|
const accountTab = screen.getByText("Account Info")
|
|
65
64
|
|
|
@@ -67,20 +66,21 @@ describe("UserAccountTabs Component", () => {
|
|
|
67
66
|
|
|
68
67
|
expect(hasActiveClass(accountTab)).toBe(true)
|
|
69
68
|
expect(hasActiveClass(datasetsTab)).toBe(false)
|
|
70
|
-
expect(hasActiveClass(notificationsTab)).toBe(false)
|
|
69
|
+
//expect(hasActiveClass(notificationsTab)).toBe(false)
|
|
70
|
+
expect(hasActiveClass(datasetsTab)).toBe(false)
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
it("should trigger animation state when a tab is clicked", async () => {
|
|
74
74
|
render(<UserAccountTabsWrapper />)
|
|
75
75
|
|
|
76
|
-
const
|
|
76
|
+
const accountTab = screen.getByText("Account Info")
|
|
77
77
|
// Utility function to check if an element has 'clicked' class - used because of CSS module discrepancies between classNames
|
|
78
78
|
const hasClickedClass = (element) => element.className.includes("clicked")
|
|
79
79
|
const tabsContainer = await screen.findByRole("list")
|
|
80
80
|
|
|
81
81
|
expect(hasClickedClass(tabsContainer)).toBe(false)
|
|
82
82
|
|
|
83
|
-
fireEvent.click(
|
|
83
|
+
fireEvent.click(accountTab)
|
|
84
84
|
|
|
85
85
|
expect(hasClickedClass(tabsContainer)).toBe(true)
|
|
86
86
|
})
|
|
@@ -6,7 +6,7 @@ interface EditListProps {
|
|
|
6
6
|
placeholder?: string
|
|
7
7
|
elements?: string[]
|
|
8
8
|
setElements: (elements: string[]) => void
|
|
9
|
-
validation?: RegExp
|
|
9
|
+
validation?: RegExp | ((value: string) => boolean)
|
|
10
10
|
validationMessage?: string
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -30,17 +30,32 @@ export const EditList: React.FC<EditListProps> = (
|
|
|
30
30
|
const removeElement = (index: number): void => {
|
|
31
31
|
setElements(elements.filter((_, i) => i !== index))
|
|
32
32
|
}
|
|
33
|
+
const isInputValid = (value: string): boolean => {
|
|
34
|
+
if (!validation) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (validation instanceof RegExp) {
|
|
39
|
+
return validation.test(value)
|
|
40
|
+
} else if (typeof validation === "function") {
|
|
41
|
+
return validation(value)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
33
46
|
|
|
34
47
|
// Add a new element to the list
|
|
35
48
|
const addElement = (): void => {
|
|
36
|
-
|
|
49
|
+
const trimmedNewElement = newElement.trim()
|
|
50
|
+
|
|
51
|
+
if (!trimmedNewElement) {
|
|
37
52
|
setWarnEmpty(true)
|
|
38
53
|
setWarnValidation(null)
|
|
39
|
-
} else if (
|
|
54
|
+
} else if (!isInputValid(trimmedNewElement)) {
|
|
40
55
|
setWarnValidation(validationMessage || "Invalid input format")
|
|
41
56
|
setWarnEmpty(false)
|
|
42
57
|
} else {
|
|
43
|
-
setElements([...elements,
|
|
58
|
+
setElements([...elements, trimmedNewElement])
|
|
44
59
|
setWarnEmpty(false)
|
|
45
60
|
setWarnValidation(null)
|
|
46
61
|
setNewElement("")
|
|
@@ -62,7 +77,13 @@ export const EditList: React.FC<EditListProps> = (
|
|
|
62
77
|
className="form-control"
|
|
63
78
|
placeholder={placeholder}
|
|
64
79
|
value={newElement}
|
|
65
|
-
onChange={(e) =>
|
|
80
|
+
onChange={(e) => {
|
|
81
|
+
setNewElement(e.target.value)
|
|
82
|
+
if (warnEmpty || warnValidation) {
|
|
83
|
+
setWarnEmpty(false)
|
|
84
|
+
setWarnValidation(null)
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
66
87
|
onKeyDown={handleKeyDown}
|
|
67
88
|
/>
|
|
68
89
|
<Button
|
|
@@ -7,7 +7,7 @@ interface EditStringProps {
|
|
|
7
7
|
setValue: (value: string) => void
|
|
8
8
|
placeholder?: string
|
|
9
9
|
closeEditing: () => void
|
|
10
|
-
validation?: RegExp
|
|
10
|
+
validation?: RegExp | ((value: string) => boolean)
|
|
11
11
|
validationMessage?: string
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -25,18 +25,35 @@ export const EditString: React.FC<EditStringProps> = (
|
|
|
25
25
|
const [warnEmpty, setWarnEmpty] = useState<string | null>(null)
|
|
26
26
|
const [warnValidation, setWarnValidation] = useState<string | null>(null)
|
|
27
27
|
|
|
28
|
+
// Helper for validation logic
|
|
29
|
+
const isInputValid = (inputValue: string): boolean => {
|
|
30
|
+
if (!validation) {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (validation instanceof RegExp) {
|
|
35
|
+
return validation.test(inputValue)
|
|
36
|
+
} else if (typeof validation === "function") {
|
|
37
|
+
return validation(inputValue)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const trimmedCurrentValue = currentValue.trim()
|
|
44
|
+
|
|
28
45
|
useEffect(() => {
|
|
29
|
-
//
|
|
30
|
-
if (value !== "" &&
|
|
46
|
+
// Logic for "empty" warning
|
|
47
|
+
if (value !== "" && trimmedCurrentValue === "") {
|
|
31
48
|
setWarnEmpty(
|
|
32
|
-
"Your input is empty. This will delete the previously saved value
|
|
49
|
+
"Your input is empty. This will delete the previously saved value.",
|
|
33
50
|
)
|
|
34
51
|
} else {
|
|
35
52
|
setWarnEmpty(null)
|
|
36
53
|
}
|
|
37
54
|
|
|
38
|
-
//
|
|
39
|
-
if (
|
|
55
|
+
// Logic for "validation" warning
|
|
56
|
+
if (trimmedCurrentValue !== "" && !isInputValid(trimmedCurrentValue)) {
|
|
40
57
|
setWarnValidation(validationMessage || "Invalid input")
|
|
41
58
|
} else {
|
|
42
59
|
setWarnValidation(null)
|
|
@@ -44,10 +61,13 @@ export const EditString: React.FC<EditStringProps> = (
|
|
|
44
61
|
}, [currentValue, value, validation, validationMessage])
|
|
45
62
|
|
|
46
63
|
const handleSave = (): void => {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
if (warnValidation) {
|
|
65
|
+
// Do not save if there's a validation error
|
|
66
|
+
return
|
|
50
67
|
}
|
|
68
|
+
|
|
69
|
+
setValue(trimmedCurrentValue)
|
|
70
|
+
closeEditing()
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
// Handle Enter key press for saving
|
|
@@ -66,7 +86,14 @@ export const EditString: React.FC<EditStringProps> = (
|
|
|
66
86
|
className="form-control"
|
|
67
87
|
placeholder={placeholder}
|
|
68
88
|
value={currentValue}
|
|
69
|
-
onChange={(e) =>
|
|
89
|
+
onChange={(e) => {
|
|
90
|
+
setCurrentValue(e.target.value)
|
|
91
|
+
// Clear warnings as user types again
|
|
92
|
+
if (warnEmpty || warnValidation) {
|
|
93
|
+
setWarnEmpty(null)
|
|
94
|
+
setWarnValidation(null)
|
|
95
|
+
}
|
|
96
|
+
}}
|
|
70
97
|
onKeyDown={handleKeyDown}
|
|
71
98
|
/>
|
|
72
99
|
<Button
|
|
@@ -77,11 +104,11 @@ export const EditString: React.FC<EditStringProps> = (
|
|
|
77
104
|
onClick={handleSave}
|
|
78
105
|
/>
|
|
79
106
|
</div>
|
|
80
|
-
{/*
|
|
81
|
-
{warnEmpty &&
|
|
107
|
+
{/* Display warning about deleting previous value if applicable */}
|
|
108
|
+
{warnEmpty && trimmedCurrentValue === "" && (
|
|
82
109
|
<small className="warning-text">{warnEmpty}</small>
|
|
83
110
|
)}
|
|
84
|
-
{/*
|
|
111
|
+
{/* Display validation error */}
|
|
85
112
|
{warnValidation && (
|
|
86
113
|
<small className="warning-text">{warnValidation}</small>
|
|
87
114
|
)}
|
|
@@ -11,7 +11,7 @@ interface EditableContentProps {
|
|
|
11
11
|
setRows: React.Dispatch<React.SetStateAction<string[] | string>>
|
|
12
12
|
className: string
|
|
13
13
|
heading: string
|
|
14
|
-
validation?: RegExp
|
|
14
|
+
validation?: RegExp | ((value: string) => boolean)
|
|
15
15
|
validationMessage?: string
|
|
16
16
|
"data-testid"?: string
|
|
17
17
|
}
|
|
@@ -33,9 +33,16 @@ export const EditableContent: React.FC<EditableContentProps> = ({
|
|
|
33
33
|
|
|
34
34
|
// Function to handle validation of user input
|
|
35
35
|
const handleValidation = (value: string): boolean => {
|
|
36
|
-
if (
|
|
37
|
-
return
|
|
36
|
+
if (!validation) {
|
|
37
|
+
return true
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
if (validation instanceof RegExp) {
|
|
41
|
+
return validation.test(value)
|
|
42
|
+
} else if (typeof validation === "function") {
|
|
43
|
+
return validation(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
return true
|
|
40
47
|
}
|
|
41
48
|
|