lightnet 3.10.3 → 3.10.5
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/CHANGELOG.md +12 -0
- package/__e2e__/admin.spec.ts +356 -100
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/package.json +9 -9
- package/src/admin/components/form/DynamicArray.tsx +74 -0
- package/src/admin/components/form/Input.tsx +36 -0
- package/src/admin/components/form/Select.tsx +22 -20
- package/src/admin/components/form/SubmitButton.tsx +20 -18
- package/src/admin/components/form/atoms/ErrorMessage.tsx +13 -0
- package/src/admin/components/form/atoms/Hint.tsx +3 -3
- package/src/admin/components/form/atoms/Label.tsx +17 -6
- package/src/admin/components/form/atoms/Legend.tsx +20 -0
- package/src/admin/components/form/hooks/use-field-error.tsx +13 -0
- package/src/admin/i18n/translations/en.yml +18 -7
- package/src/admin/pages/media/EditForm.tsx +52 -68
- package/src/admin/pages/media/EditRoute.astro +35 -11
- package/src/admin/pages/media/fields/Authors.tsx +43 -0
- package/src/admin/pages/media/fields/Categories.tsx +64 -0
- package/src/admin/pages/media/fields/Collections.tsx +103 -0
- package/src/admin/pages/media/media-item-store.ts +14 -7
- package/src/admin/types/media-item.ts +38 -2
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/HeroSection.astro +1 -1
- package/src/components/HighlightSection.astro +1 -1
- package/src/components/MediaGallerySection.astro +3 -3
- package/src/components/MediaList.astro +2 -2
- package/src/components/SearchInput.astro +1 -1
- package/src/content/get-categories.ts +18 -3
- package/src/i18n/react/i18n-context.ts +14 -12
- package/src/i18n/resolve-language.ts +1 -1
- package/src/layouts/MarkdownPage.astro +1 -1
- package/src/layouts/Page.astro +3 -2
- package/src/layouts/components/LanguagePicker.astro +1 -1
- package/src/layouts/components/Menu.astro +1 -1
- package/src/layouts/components/PageNavigation.astro +1 -1
- package/src/pages/details-page/components/main-details/OpenButton.astro +1 -1
- package/src/pages/details-page/components/more-details/Languages.astro +2 -2
- package/src/pages/search-page/components/LoadingSkeleton.tsx +1 -1
- package/src/pages/search-page/components/SearchFilter.astro +7 -7
- package/src/pages/search-page/components/SearchFilter.tsx +5 -5
- package/src/pages/search-page/components/SearchList.astro +4 -4
- package/src/pages/search-page/components/SearchListItem.tsx +5 -5
- package/src/pages/search-page/components/Select.tsx +4 -4
- package/src/pages/search-page/hooks/use-search.ts +4 -4
- package/src/admin/components/form/TextInput.tsx +0 -34
- package/src/admin/components/form/atoms/FieldErrors.tsx +0 -22
- package/src/admin/components/form/form-context.ts +0 -4
- package/src/admin/components/form/index.ts +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 3.10.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#331](https://github.com/LightNetDev/LightNet/pull/331) [`cdacfdf`](https://github.com/LightNetDev/LightNet/commit/cdacfdfca311469248cb7d1ac0b46f92d9fd5521) Thanks [@smn-cds](https://github.com/smn-cds)! - Experimenatl Admin UI: support editing categories and collections
|
|
8
|
+
|
|
9
|
+
## 3.10.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#329](https://github.com/LightNetDev/LightNet/pull/329) [`1730ef6`](https://github.com/LightNetDev/LightNet/commit/1730ef6f769d4c0d8fb22acdb86eaf19c3a32b2c) Thanks [@smn-cds](https://github.com/smn-cds)! - Improve performance of react i18n context.
|
|
14
|
+
|
|
3
15
|
## 3.10.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/__e2e__/admin.spec.ts
CHANGED
|
@@ -1,113 +1,369 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
|
|
3
|
+
import { expect, type Page } from "@playwright/test"
|
|
2
4
|
|
|
3
5
|
import { lightnetTest } from "./test-utils"
|
|
4
6
|
|
|
5
7
|
const test = lightnetTest("./fixtures/basics/")
|
|
8
|
+
const faithfulFreestyleMediaUrl = new URL(
|
|
9
|
+
"./fixtures/basics/src/content/media/faithful-freestyle--en.json",
|
|
10
|
+
import.meta.url,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
|
-
test("
|
|
8
|
-
page,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
test.describe("Edit button on details page", () => {
|
|
14
|
+
test("Should not show `Edit` button on details page by default.", async ({
|
|
15
|
+
page,
|
|
16
|
+
startLightnet,
|
|
17
|
+
}) => {
|
|
18
|
+
await startLightnet()
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
await page.getByRole("link", { name: "Faithful Freestyle" }).click()
|
|
21
|
+
await expect(
|
|
22
|
+
page.getByRole("heading", { name: "Faithful Freestyle" }),
|
|
23
|
+
).toBeVisible()
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
})
|
|
25
|
+
const editButton = page.locator("#edit-btn")
|
|
26
|
+
await expect(editButton).toBeHidden()
|
|
27
|
+
})
|
|
21
28
|
|
|
22
|
-
test("Should show `Edit` button on book details page after visiting `/en/admin/` path.", async ({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
await page.goto(ln.resolveURL("/en/admin/"))
|
|
29
|
-
await expect(
|
|
30
|
-
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
31
|
-
).toBeVisible()
|
|
32
|
-
|
|
33
|
-
await page.goto(ln.resolveURL("/en/media/faithful-freestyle--en"))
|
|
34
|
-
await expect(
|
|
35
|
-
page.getByRole("heading", { name: "Faithful Freestyle" }),
|
|
36
|
-
).toBeVisible()
|
|
37
|
-
|
|
38
|
-
const editButton = page.locator("#edit-btn")
|
|
39
|
-
await expect(editButton).toBeVisible()
|
|
40
|
-
await expect(editButton).toHaveAttribute(
|
|
41
|
-
"href",
|
|
42
|
-
"/en/admin/media/faithful-freestyle--en",
|
|
43
|
-
)
|
|
44
|
-
})
|
|
29
|
+
test("Should show `Edit` button on book details page after visiting `/en/admin/` path.", async ({
|
|
30
|
+
page,
|
|
31
|
+
startLightnet,
|
|
32
|
+
}) => {
|
|
33
|
+
const ln = await startLightnet()
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
68
|
-
|
|
35
|
+
await page.goto(ln.resolveURL("/en/admin/"))
|
|
36
|
+
await expect(
|
|
37
|
+
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
38
|
+
).toBeVisible()
|
|
39
|
+
|
|
40
|
+
await page.goto(ln.resolveURL("/en/media/faithful-freestyle--en"))
|
|
41
|
+
await expect(
|
|
42
|
+
page.getByRole("heading", { name: "Faithful Freestyle" }),
|
|
43
|
+
).toBeVisible()
|
|
44
|
+
|
|
45
|
+
const editButton = page.locator("#edit-btn")
|
|
46
|
+
await expect(editButton).toBeVisible()
|
|
47
|
+
await expect(editButton).toHaveAttribute(
|
|
48
|
+
"href",
|
|
49
|
+
"/en/admin/media/faithful-freestyle--en",
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("Should show `Edit` button on video details page after visiting `/en/admin/` path.", async ({
|
|
54
|
+
page,
|
|
55
|
+
startLightnet,
|
|
56
|
+
}) => {
|
|
57
|
+
const ln = await startLightnet()
|
|
58
|
+
|
|
59
|
+
await page.goto(ln.resolveURL("/en/admin/"))
|
|
60
|
+
await expect(
|
|
61
|
+
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
62
|
+
).toBeVisible()
|
|
63
|
+
|
|
64
|
+
await page.goto(ln.resolveURL("/en/media/how-to-kickflip--de"))
|
|
65
|
+
await expect(
|
|
66
|
+
page.getByRole("heading", { name: "Kickflip Anleitung" }),
|
|
67
|
+
).toBeVisible()
|
|
68
|
+
|
|
69
|
+
const editButton = page.locator("#edit-btn")
|
|
70
|
+
await expect(editButton).toBeVisible()
|
|
71
|
+
await expect(editButton).toHaveAttribute(
|
|
72
|
+
"href",
|
|
73
|
+
"/en/admin/media/how-to-kickflip--de",
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("Should show `Edit` button on audio details page after visiting `/en/admin/` path.", async ({
|
|
78
|
+
page,
|
|
79
|
+
startLightnet,
|
|
80
|
+
}) => {
|
|
81
|
+
const ln = await startLightnet()
|
|
82
|
+
|
|
83
|
+
await page.goto(ln.resolveURL("/en/admin/"))
|
|
84
|
+
await expect(
|
|
85
|
+
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
86
|
+
).toBeVisible()
|
|
87
|
+
|
|
88
|
+
await page.goto(ln.resolveURL("/en/media/skate-sounds--en"))
|
|
89
|
+
await expect(
|
|
90
|
+
page.getByRole("heading", { name: "Skate Sounds" }),
|
|
91
|
+
).toBeVisible()
|
|
92
|
+
|
|
93
|
+
const editButton = page.locator("#edit-btn")
|
|
94
|
+
await expect(editButton).toBeVisible()
|
|
95
|
+
await expect(editButton).toHaveAttribute(
|
|
96
|
+
"href",
|
|
97
|
+
"/en/admin/media/skate-sounds--en",
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test("Edit button on details page should navigate to media item edit page", async ({
|
|
102
|
+
page,
|
|
103
|
+
startLightnet,
|
|
104
|
+
}) => {
|
|
105
|
+
const ln = await startLightnet()
|
|
106
|
+
|
|
107
|
+
await page.goto(ln.resolveURL("/en/admin/"))
|
|
108
|
+
await page.goto(ln.resolveURL("/en/media/faithful-freestyle--en"))
|
|
69
109
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await page.goto(ln.resolveURL("/en/media/skate-sounds--en"))
|
|
82
|
-
await expect(
|
|
83
|
-
page.getByRole("heading", { name: "Skate Sounds" }),
|
|
84
|
-
).toBeVisible()
|
|
85
|
-
|
|
86
|
-
const editButton = page.locator("#edit-btn")
|
|
87
|
-
await expect(editButton).toBeVisible()
|
|
88
|
-
await expect(editButton).toHaveAttribute(
|
|
89
|
-
"href",
|
|
90
|
-
"/en/admin/media/skate-sounds--en",
|
|
91
|
-
)
|
|
110
|
+
const editButton = page.locator("#edit-btn")
|
|
111
|
+
await expect(editButton).toBeVisible()
|
|
112
|
+
|
|
113
|
+
await editButton.click()
|
|
114
|
+
await expect(page).toHaveURL(
|
|
115
|
+
ln.resolveURL("/en/admin/media/faithful-freestyle--en"),
|
|
116
|
+
)
|
|
117
|
+
await expect(
|
|
118
|
+
page.getByText("Edit media item", { exact: false }),
|
|
119
|
+
).toBeVisible()
|
|
120
|
+
})
|
|
92
121
|
})
|
|
93
122
|
|
|
94
|
-
test("
|
|
95
|
-
page
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
test.describe("Media item edit page", () => {
|
|
124
|
+
const recordWriteFile = async (page: Page) => {
|
|
125
|
+
type WriteFileRequest = { url: string; body: unknown }
|
|
126
|
+
let resolveWriteFileRequest: ((value: WriteFileRequest) => void) | null =
|
|
127
|
+
null
|
|
128
|
+
const writeFileRequestPromise = new Promise<WriteFileRequest>((resolve) => {
|
|
129
|
+
resolveWriteFileRequest = resolve
|
|
130
|
+
})
|
|
131
|
+
await page.route("**/api/internal/fs/write-file?*", async (route) => {
|
|
132
|
+
const request = route.request()
|
|
133
|
+
resolveWriteFileRequest?.({
|
|
134
|
+
url: request.url(),
|
|
135
|
+
body: JSON.parse(request.postData() ?? ""),
|
|
136
|
+
})
|
|
137
|
+
await route.fulfill({
|
|
138
|
+
status: 200,
|
|
139
|
+
contentType: "application/json",
|
|
140
|
+
body: JSON.stringify({ status: "ok" }),
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
return () => writeFileRequestPromise
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const getPublishButton = (page: Page) =>
|
|
147
|
+
page.getByRole("button", { name: "Publish Changes" }).first()
|
|
148
|
+
|
|
149
|
+
const expectPublishedMessage = (page: Page) =>
|
|
150
|
+
expect(
|
|
151
|
+
page.getByRole("button", { name: "Published" }).first(),
|
|
152
|
+
).toBeVisible()
|
|
153
|
+
|
|
154
|
+
test("should edit title", async ({ page, startLightnet }) => {
|
|
155
|
+
const ln = await startLightnet()
|
|
156
|
+
|
|
157
|
+
const writeFileRequest = await recordWriteFile(page)
|
|
158
|
+
|
|
159
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
160
|
+
|
|
161
|
+
const updatedTitle = "Faithful Freestyle (Edited)"
|
|
162
|
+
const titleInput = page.getByLabel("Title")
|
|
163
|
+
await expect(titleInput).toHaveValue("Faithful Freestyle")
|
|
164
|
+
await titleInput.fill(updatedTitle)
|
|
165
|
+
|
|
166
|
+
const saveButton = getPublishButton(page)
|
|
167
|
+
await expect(saveButton).toBeEnabled()
|
|
168
|
+
await saveButton.click()
|
|
169
|
+
|
|
170
|
+
const { url, body } = await writeFileRequest()
|
|
171
|
+
expect(url).toContain(
|
|
172
|
+
"/api/internal/fs/write-file?path=src%2Fcontent%2Fmedia%2Ffaithful-freestyle--en.json",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
const expectedMediaItem = JSON.parse(
|
|
176
|
+
await readFile(faithfulFreestyleMediaUrl, "utf-8"),
|
|
177
|
+
)
|
|
178
|
+
expect(body).toEqual({
|
|
179
|
+
...expectedMediaItem,
|
|
180
|
+
title: updatedTitle,
|
|
181
|
+
})
|
|
182
|
+
await expectPublishedMessage(page)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test("Should update media type", async ({ page, startLightnet }) => {
|
|
186
|
+
const ln = await startLightnet()
|
|
187
|
+
const writeFileRequest = await recordWriteFile(page)
|
|
188
|
+
|
|
189
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
190
|
+
|
|
191
|
+
const typeSelect = page.getByLabel("Type")
|
|
192
|
+
await expect(typeSelect).toHaveValue("book")
|
|
193
|
+
await typeSelect.selectOption("video")
|
|
194
|
+
|
|
195
|
+
const saveButton = getPublishButton(page)
|
|
196
|
+
await expect(saveButton).toBeEnabled()
|
|
197
|
+
await saveButton.click()
|
|
198
|
+
|
|
199
|
+
const { body } = await writeFileRequest()
|
|
200
|
+
const expectedMediaItem = JSON.parse(
|
|
201
|
+
await readFile(faithfulFreestyleMediaUrl, "utf-8"),
|
|
202
|
+
)
|
|
203
|
+
expect(body).toEqual({
|
|
204
|
+
...expectedMediaItem,
|
|
205
|
+
type: "video",
|
|
206
|
+
})
|
|
207
|
+
await expectPublishedMessage(page)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test("Should update author name", async ({ page, startLightnet }) => {
|
|
211
|
+
const ln = await startLightnet()
|
|
212
|
+
const writeFileRequest = await recordWriteFile(page)
|
|
213
|
+
|
|
214
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
215
|
+
|
|
216
|
+
const authorsFieldset = page.getByRole("group", { name: "Authors" })
|
|
217
|
+
const firstAuthorInput = authorsFieldset.getByRole("textbox").first()
|
|
218
|
+
const updatedAuthor = "Sk8 Ministries International"
|
|
219
|
+
await expect(firstAuthorInput).toHaveValue("Sk8 Ministries")
|
|
220
|
+
await firstAuthorInput.fill(updatedAuthor)
|
|
221
|
+
|
|
222
|
+
const saveButton = getPublishButton(page)
|
|
223
|
+
await expect(saveButton).toBeEnabled()
|
|
224
|
+
await saveButton.click()
|
|
225
|
+
|
|
226
|
+
const { body } = await writeFileRequest()
|
|
227
|
+
const expectedMediaItem = JSON.parse(
|
|
228
|
+
await readFile(faithfulFreestyleMediaUrl, "utf-8"),
|
|
229
|
+
)
|
|
230
|
+
expect(body).toEqual({
|
|
231
|
+
...expectedMediaItem,
|
|
232
|
+
authors: [updatedAuthor],
|
|
233
|
+
})
|
|
234
|
+
await expectPublishedMessage(page)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test("Should add author", async ({ page, startLightnet }) => {
|
|
238
|
+
const ln = await startLightnet()
|
|
239
|
+
const writeFileRequest = await recordWriteFile(page)
|
|
240
|
+
|
|
241
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
242
|
+
|
|
243
|
+
const authorsFieldset = page.getByRole("group", { name: "Authors" })
|
|
244
|
+
const addAuthorButton = page.getByRole("button", { name: "Add Author" })
|
|
245
|
+
await addAuthorButton.click()
|
|
246
|
+
const newAuthorInput = authorsFieldset.getByRole("textbox").last()
|
|
247
|
+
const additionalAuthor = "Tony Hawk"
|
|
248
|
+
await newAuthorInput.fill(additionalAuthor)
|
|
249
|
+
|
|
250
|
+
const saveButton = getPublishButton(page)
|
|
251
|
+
await expect(saveButton).toBeEnabled()
|
|
252
|
+
await saveButton.click()
|
|
253
|
+
|
|
254
|
+
const { body } = await writeFileRequest()
|
|
255
|
+
const expectedMediaItem = JSON.parse(
|
|
256
|
+
await readFile(faithfulFreestyleMediaUrl, "utf-8"),
|
|
257
|
+
)
|
|
258
|
+
expect(body).toEqual({
|
|
259
|
+
...expectedMediaItem,
|
|
260
|
+
authors: ["Sk8 Ministries", additionalAuthor],
|
|
261
|
+
})
|
|
262
|
+
await expectPublishedMessage(page)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("Should remove author", async ({ page, startLightnet }) => {
|
|
266
|
+
const ln = await startLightnet()
|
|
267
|
+
const writeFileRequest = await recordWriteFile(page)
|
|
268
|
+
|
|
269
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
270
|
+
|
|
271
|
+
const authorsFieldset = page.getByRole("group", { name: "Authors" })
|
|
272
|
+
const addAuthorButton = page.getByRole("button", { name: "Add Author" })
|
|
273
|
+
const replacementAuthor = "Skate Evangelists"
|
|
274
|
+
await addAuthorButton.click()
|
|
275
|
+
const addedAuthorInput = authorsFieldset.getByRole("textbox").last()
|
|
276
|
+
await addedAuthorInput.fill(replacementAuthor)
|
|
277
|
+
|
|
278
|
+
const removeButtons = authorsFieldset.getByRole("button", {
|
|
279
|
+
name: "Remove",
|
|
280
|
+
})
|
|
281
|
+
await removeButtons.first().click()
|
|
282
|
+
|
|
283
|
+
const saveButton = getPublishButton(page)
|
|
284
|
+
await expect(saveButton).toBeEnabled()
|
|
285
|
+
await saveButton.click()
|
|
286
|
+
|
|
287
|
+
const { body } = await writeFileRequest()
|
|
288
|
+
const expectedMediaItem = JSON.parse(
|
|
289
|
+
await readFile(faithfulFreestyleMediaUrl, "utf-8"),
|
|
290
|
+
)
|
|
291
|
+
expect(body).toEqual({
|
|
292
|
+
...expectedMediaItem,
|
|
293
|
+
authors: [replacementAuthor],
|
|
294
|
+
})
|
|
295
|
+
await expectPublishedMessage(page)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test("should show error message if common id is set empty", async ({
|
|
299
|
+
page,
|
|
300
|
+
startLightnet,
|
|
301
|
+
}) => {
|
|
302
|
+
const ln = await startLightnet()
|
|
303
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
304
|
+
|
|
305
|
+
const commonIdInput = page.getByLabel("Common ID")
|
|
306
|
+
await expect(commonIdInput).toHaveValue("faithful-freestyle")
|
|
307
|
+
|
|
308
|
+
await commonIdInput.fill("")
|
|
309
|
+
await commonIdInput.blur()
|
|
310
|
+
|
|
311
|
+
await expect(
|
|
312
|
+
page
|
|
313
|
+
.getByRole("alert")
|
|
314
|
+
.filter({ hasText: "Please enter at least one character." }),
|
|
315
|
+
).toBeVisible()
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test("should focus invalid field when submitting invalid form data", async ({
|
|
319
|
+
page,
|
|
320
|
+
startLightnet,
|
|
321
|
+
}) => {
|
|
322
|
+
const ln = await startLightnet()
|
|
323
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
324
|
+
|
|
325
|
+
const categoriesFieldset = page.getByRole("group", { name: "Categories" })
|
|
326
|
+
await page.getByRole("button", { name: "Add Category" }).click()
|
|
327
|
+
const newCategorySelect = categoriesFieldset.getByRole("combobox").last()
|
|
328
|
+
await expect(newCategorySelect).toHaveValue("")
|
|
329
|
+
|
|
330
|
+
// move focus away so the submission handler needs to return focus
|
|
331
|
+
await page.getByLabel("Title").click()
|
|
332
|
+
|
|
333
|
+
const saveButton = getPublishButton(page)
|
|
334
|
+
await saveButton.click()
|
|
335
|
+
|
|
336
|
+
await expect(
|
|
337
|
+
page.getByRole("alert").filter({ hasText: "This field is required." }),
|
|
338
|
+
).toBeVisible()
|
|
339
|
+
await expect(newCategorySelect).toBeFocused()
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test("should not allow assigning duplicate categories", async ({
|
|
343
|
+
page,
|
|
344
|
+
startLightnet,
|
|
345
|
+
}) => {
|
|
346
|
+
const ln = await startLightnet()
|
|
347
|
+
await page.goto(ln.resolveURL("/en/admin/media/faithful-freestyle--en"))
|
|
348
|
+
|
|
349
|
+
const categoriesFieldset = page.getByRole("group", { name: "Categories" })
|
|
350
|
+
await page.getByRole("button", { name: "Add Category" }).click()
|
|
351
|
+
|
|
352
|
+
const categorySelects = categoriesFieldset.getByRole("combobox")
|
|
353
|
+
const firstCategoryValue = await categorySelects.first().inputValue()
|
|
354
|
+
const duplicateCategorySelect = categorySelects.last()
|
|
355
|
+
await duplicateCategorySelect.selectOption(firstCategoryValue)
|
|
356
|
+
|
|
357
|
+
const publishButton = getPublishButton(page)
|
|
358
|
+
await publishButton.click()
|
|
359
|
+
|
|
360
|
+
const duplicateCategoryError = categoriesFieldset
|
|
361
|
+
.getByRole("alert")
|
|
362
|
+
.filter({ hasText: "Please choose a different value for each entry." })
|
|
363
|
+
await expect(duplicateCategoryError).toBeVisible()
|
|
364
|
+
await expect(duplicateCategorySelect).toHaveAttribute(
|
|
365
|
+
"aria-invalid",
|
|
366
|
+
"true",
|
|
367
|
+
)
|
|
368
|
+
})
|
|
113
369
|
})
|
|
@@ -10,9 +10,9 @@ case `uname` in
|
|
|
10
10
|
esac
|
|
11
11
|
|
|
12
12
|
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.
|
|
13
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.15.5_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.15.5_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules"
|
|
14
14
|
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.
|
|
15
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.15.5_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.15.5_@types+node@24.10.1_jiti@2.4.2_lightningcss@1.29.1_rollup@4.53.2_terser@5.39.0_typescript@5.9.3_yaml@2.8.1/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
16
|
fi
|
|
17
17
|
if [ -x "$basedir/node" ]; then
|
|
18
18
|
exec "$basedir/node" "$basedir/../astro/astro.js" "$@"
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"version": "0.0.1",
|
|
5
5
|
"private": "true",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@astrojs/react": "^4.4.
|
|
7
|
+
"@astrojs/react": "^4.4.2",
|
|
8
8
|
"@astrojs/tailwind": "^6.0.2",
|
|
9
|
-
"@lightnet/decap-admin": "^3.1.
|
|
10
|
-
"astro": "^5.
|
|
11
|
-
"lightnet": "^3.10.
|
|
9
|
+
"@lightnet/decap-admin": "^3.1.4",
|
|
10
|
+
"astro": "^5.15.5",
|
|
11
|
+
"lightnet": "^3.10.4",
|
|
12
12
|
"react": "^19.2.0",
|
|
13
13
|
"react-dom": "^19.2.0",
|
|
14
|
-
"sharp": "^0.34.
|
|
14
|
+
"sharp": "^0.34.5",
|
|
15
15
|
"tailwindcss": "^3.4.18",
|
|
16
16
|
"typescript": "^5.9.3"
|
|
17
17
|
},
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "LightNet makes it easy to run your own digital media library.",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "3.10.
|
|
6
|
+
"version": "3.10.5",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -45,28 +45,28 @@
|
|
|
45
45
|
"tailwindcss": ">=3.4.0 <4.0.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@astrojs/react": "^4.4.
|
|
48
|
+
"@astrojs/react": "^4.4.2",
|
|
49
49
|
"@astrojs/tailwind": "^6.0.2",
|
|
50
|
+
"@hookform/resolvers": "^5.2.2",
|
|
50
51
|
"@iconify-json/mdi": "^1.2.3",
|
|
51
52
|
"@iconify/tailwind": "^1.2.0",
|
|
52
53
|
"@tailwindcss/typography": "^0.5.19",
|
|
53
|
-
"@tanstack/react-form": "^1.23.7",
|
|
54
|
-
"@tanstack/react-query": "^5.90.5",
|
|
55
54
|
"@tanstack/react-virtual": "^3.13.12",
|
|
56
55
|
"daisyui": "^4.12.24",
|
|
57
56
|
"embla-carousel": "^8.6.0",
|
|
58
57
|
"embla-carousel-wheel-gestures": "^8.1.0",
|
|
59
58
|
"fuse.js": "^7.1.0",
|
|
60
|
-
"i18next": "^25.6.
|
|
61
|
-
"marked": "^16.4.
|
|
59
|
+
"i18next": "^25.6.2",
|
|
60
|
+
"marked": "^16.4.2",
|
|
61
|
+
"react-hook-form": "^7.66.0",
|
|
62
62
|
"yaml": "^2.8.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@playwright/test": "^1.56.1",
|
|
66
|
-
"@types/node": "^22.
|
|
67
|
-
"@types/react": "^19.2.
|
|
66
|
+
"@types/node": "^22.19.1",
|
|
67
|
+
"@types/react": "^19.2.4",
|
|
68
68
|
"typescript": "^5.9.3",
|
|
69
|
-
"vitest": "^
|
|
69
|
+
"vitest": "^4.0.8"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"node": ">=22"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import {
|
|
3
|
+
type ArrayPath,
|
|
4
|
+
type Control,
|
|
5
|
+
type FieldValues,
|
|
6
|
+
useFieldArray,
|
|
7
|
+
type UseFieldArrayAppend,
|
|
8
|
+
} from "react-hook-form"
|
|
9
|
+
|
|
10
|
+
import Icon from "../../../components/Icon"
|
|
11
|
+
import { useI18n } from "../../../i18n/react/useI18n"
|
|
12
|
+
import ErrorMessage from "./atoms/ErrorMessage"
|
|
13
|
+
import Hint from "./atoms/Hint"
|
|
14
|
+
import Legend from "./atoms/Legend"
|
|
15
|
+
import { useFieldError } from "./hooks/use-field-error"
|
|
16
|
+
|
|
17
|
+
export default function DynamicArray<TFieldValues extends FieldValues>({
|
|
18
|
+
control,
|
|
19
|
+
name,
|
|
20
|
+
label,
|
|
21
|
+
hint,
|
|
22
|
+
renderElement,
|
|
23
|
+
addButton,
|
|
24
|
+
}: {
|
|
25
|
+
name: ArrayPath<TFieldValues>
|
|
26
|
+
label: string
|
|
27
|
+
hint?: string
|
|
28
|
+
control: Control<TFieldValues>
|
|
29
|
+
renderElement: (index: number) => ReactNode
|
|
30
|
+
addButton: {
|
|
31
|
+
label: string
|
|
32
|
+
onClick: (
|
|
33
|
+
append: UseFieldArrayAppend<TFieldValues, ArrayPath<TFieldValues>>,
|
|
34
|
+
elementIndex: number,
|
|
35
|
+
) => void
|
|
36
|
+
}
|
|
37
|
+
}) {
|
|
38
|
+
const { fields, append, remove } = useFieldArray({
|
|
39
|
+
name,
|
|
40
|
+
control,
|
|
41
|
+
})
|
|
42
|
+
const { t } = useI18n()
|
|
43
|
+
const errorMessage = useFieldError({ control, name })
|
|
44
|
+
return (
|
|
45
|
+
<fieldset key={name}>
|
|
46
|
+
<Legend label={label} />
|
|
47
|
+
<div className="flex w-full flex-col divide-y divide-gray-300 overflow-hidden rounded-lg border border-gray-300 bg-gray-100 shadow-sm">
|
|
48
|
+
{fields.map((field, index) => (
|
|
49
|
+
<div className="flex w-full items-center gap-2 p-2" key={field.id}>
|
|
50
|
+
<div className="flex grow flex-col">{renderElement(index)}</div>
|
|
51
|
+
<button
|
|
52
|
+
className="flex items-center p-2 text-gray-600 transition-colors ease-in-out hover:text-rose-800"
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={() => remove(index)}
|
|
55
|
+
>
|
|
56
|
+
<Icon className="mdi--remove" ariaLabel={t("ln.admin.remove")} />
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
))}
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="p-4 text-sm font-bold text-gray-500 transition-colors ease-in-out hover:bg-gray-200"
|
|
63
|
+
onClick={() => {
|
|
64
|
+
addButton.onClick(append, fields.length)
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{t(addButton.label)}
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
<ErrorMessage message={errorMessage} />
|
|
71
|
+
<Hint label={hint} />
|
|
72
|
+
</fieldset>
|
|
73
|
+
)
|
|
74
|
+
}
|