lightnet 3.10.4 → 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 +6 -0
- package/__e2e__/admin.spec.ts +71 -12
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/package.json +2 -2
- package/package.json +4 -5
- package/src/admin/components/form/DynamicArray.tsx +74 -0
- package/src/admin/components/form/Input.tsx +5 -5
- package/src/admin/components/form/Select.tsx +8 -10
- package/src/admin/components/form/SubmitButton.tsx +9 -12
- package/src/admin/components/form/atoms/ErrorMessage.tsx +7 -28
- package/src/admin/components/form/atoms/Hint.tsx +2 -2
- package/src/admin/components/form/atoms/Label.tsx +5 -1
- package/src/admin/components/form/atoms/Legend.tsx +12 -2
- package/src/admin/components/form/hooks/use-field-error.tsx +3 -11
- package/src/admin/i18n/translations/en.yml +15 -5
- package/src/admin/pages/media/EditForm.tsx +20 -4
- package/src/admin/pages/media/EditRoute.astro +28 -9
- package/src/admin/pages/media/fields/Authors.tsx +35 -50
- 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 +6 -1
- package/src/admin/types/media-item.ts +34 -2
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/MediaGallerySection.astro +3 -3
- package/src/components/MediaList.astro +2 -2
- package/src/content/get-categories.ts +18 -3
- package/src/i18n/resolve-language.ts +1 -1
- package/src/layouts/Page.astro +3 -2
- package/src/layouts/components/LanguagePicker.astro +1 -1
- package/src/pages/details-page/components/more-details/Languages.astro +2 -2
- package/src/pages/search-page/components/SearchFilter.astro +7 -7
- package/src/pages/search-page/components/SearchFilter.tsx +4 -4
- package/src/pages/search-page/components/SearchList.astro +4 -4
- package/src/pages/search-page/components/SearchListItem.tsx +4 -4
- package/src/pages/search-page/components/Select.tsx +3 -3
- package/src/pages/search-page/hooks/use-search.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 3.10.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/__e2e__/admin.spec.ts
CHANGED
|
@@ -143,6 +143,14 @@ test.describe("Media item edit page", () => {
|
|
|
143
143
|
return () => writeFileRequestPromise
|
|
144
144
|
}
|
|
145
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
|
+
|
|
146
154
|
test("should edit title", async ({ page, startLightnet }) => {
|
|
147
155
|
const ln = await startLightnet()
|
|
148
156
|
|
|
@@ -155,7 +163,7 @@ test.describe("Media item edit page", () => {
|
|
|
155
163
|
await expect(titleInput).toHaveValue("Faithful Freestyle")
|
|
156
164
|
await titleInput.fill(updatedTitle)
|
|
157
165
|
|
|
158
|
-
const saveButton = page
|
|
166
|
+
const saveButton = getPublishButton(page)
|
|
159
167
|
await expect(saveButton).toBeEnabled()
|
|
160
168
|
await saveButton.click()
|
|
161
169
|
|
|
@@ -171,7 +179,7 @@ test.describe("Media item edit page", () => {
|
|
|
171
179
|
...expectedMediaItem,
|
|
172
180
|
title: updatedTitle,
|
|
173
181
|
})
|
|
174
|
-
await
|
|
182
|
+
await expectPublishedMessage(page)
|
|
175
183
|
})
|
|
176
184
|
|
|
177
185
|
test("Should update media type", async ({ page, startLightnet }) => {
|
|
@@ -184,7 +192,7 @@ test.describe("Media item edit page", () => {
|
|
|
184
192
|
await expect(typeSelect).toHaveValue("book")
|
|
185
193
|
await typeSelect.selectOption("video")
|
|
186
194
|
|
|
187
|
-
const saveButton = page
|
|
195
|
+
const saveButton = getPublishButton(page)
|
|
188
196
|
await expect(saveButton).toBeEnabled()
|
|
189
197
|
await saveButton.click()
|
|
190
198
|
|
|
@@ -196,7 +204,7 @@ test.describe("Media item edit page", () => {
|
|
|
196
204
|
...expectedMediaItem,
|
|
197
205
|
type: "video",
|
|
198
206
|
})
|
|
199
|
-
await
|
|
207
|
+
await expectPublishedMessage(page)
|
|
200
208
|
})
|
|
201
209
|
|
|
202
210
|
test("Should update author name", async ({ page, startLightnet }) => {
|
|
@@ -211,7 +219,7 @@ test.describe("Media item edit page", () => {
|
|
|
211
219
|
await expect(firstAuthorInput).toHaveValue("Sk8 Ministries")
|
|
212
220
|
await firstAuthorInput.fill(updatedAuthor)
|
|
213
221
|
|
|
214
|
-
const saveButton = page
|
|
222
|
+
const saveButton = getPublishButton(page)
|
|
215
223
|
await expect(saveButton).toBeEnabled()
|
|
216
224
|
await saveButton.click()
|
|
217
225
|
|
|
@@ -223,7 +231,7 @@ test.describe("Media item edit page", () => {
|
|
|
223
231
|
...expectedMediaItem,
|
|
224
232
|
authors: [updatedAuthor],
|
|
225
233
|
})
|
|
226
|
-
await
|
|
234
|
+
await expectPublishedMessage(page)
|
|
227
235
|
})
|
|
228
236
|
|
|
229
237
|
test("Should add author", async ({ page, startLightnet }) => {
|
|
@@ -239,7 +247,7 @@ test.describe("Media item edit page", () => {
|
|
|
239
247
|
const additionalAuthor = "Tony Hawk"
|
|
240
248
|
await newAuthorInput.fill(additionalAuthor)
|
|
241
249
|
|
|
242
|
-
const saveButton = page
|
|
250
|
+
const saveButton = getPublishButton(page)
|
|
243
251
|
await expect(saveButton).toBeEnabled()
|
|
244
252
|
await saveButton.click()
|
|
245
253
|
|
|
@@ -251,7 +259,7 @@ test.describe("Media item edit page", () => {
|
|
|
251
259
|
...expectedMediaItem,
|
|
252
260
|
authors: ["Sk8 Ministries", additionalAuthor],
|
|
253
261
|
})
|
|
254
|
-
await
|
|
262
|
+
await expectPublishedMessage(page)
|
|
255
263
|
})
|
|
256
264
|
|
|
257
265
|
test("Should remove author", async ({ page, startLightnet }) => {
|
|
@@ -272,7 +280,7 @@ test.describe("Media item edit page", () => {
|
|
|
272
280
|
})
|
|
273
281
|
await removeButtons.first().click()
|
|
274
282
|
|
|
275
|
-
const saveButton = page
|
|
283
|
+
const saveButton = getPublishButton(page)
|
|
276
284
|
await expect(saveButton).toBeEnabled()
|
|
277
285
|
await saveButton.click()
|
|
278
286
|
|
|
@@ -284,7 +292,7 @@ test.describe("Media item edit page", () => {
|
|
|
284
292
|
...expectedMediaItem,
|
|
285
293
|
authors: [replacementAuthor],
|
|
286
294
|
})
|
|
287
|
-
await
|
|
295
|
+
await expectPublishedMessage(page)
|
|
288
296
|
})
|
|
289
297
|
|
|
290
298
|
test("should show error message if common id is set empty", async ({
|
|
@@ -303,8 +311,59 @@ test.describe("Media item edit page", () => {
|
|
|
303
311
|
await expect(
|
|
304
312
|
page
|
|
305
313
|
.getByRole("alert")
|
|
306
|
-
.filter({ hasText: "
|
|
314
|
+
.filter({ hasText: "Please enter at least one character." }),
|
|
307
315
|
).toBeVisible()
|
|
308
|
-
|
|
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
|
+
)
|
|
309
368
|
})
|
|
310
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.15.
|
|
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.
|
|
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" "$@"
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"@astrojs/react": "^4.4.2",
|
|
8
8
|
"@astrojs/tailwind": "^6.0.2",
|
|
9
9
|
"@lightnet/decap-admin": "^3.1.4",
|
|
10
|
-
"astro": "^5.15.
|
|
11
|
-
"lightnet": "^3.10.
|
|
10
|
+
"astro": "^5.15.5",
|
|
11
|
+
"lightnet": "^3.10.4",
|
|
12
12
|
"react": "^19.2.0",
|
|
13
13
|
"react-dom": "^19.2.0",
|
|
14
14
|
"sharp": "^0.34.5",
|
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",
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@astrojs/react": "^4.4.2",
|
|
49
49
|
"@astrojs/tailwind": "^6.0.2",
|
|
50
|
-
"@hookform/error-message": "^2.0.1",
|
|
51
50
|
"@hookform/resolvers": "^5.2.2",
|
|
52
51
|
"@iconify-json/mdi": "^1.2.3",
|
|
53
52
|
"@iconify/tailwind": "^1.2.0",
|
|
@@ -57,15 +56,15 @@
|
|
|
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.
|
|
59
|
+
"i18next": "^25.6.2",
|
|
61
60
|
"marked": "^16.4.2",
|
|
62
61
|
"react-hook-form": "^7.66.0",
|
|
63
62
|
"yaml": "^2.8.1"
|
|
64
63
|
},
|
|
65
64
|
"devDependencies": {
|
|
66
65
|
"@playwright/test": "^1.56.1",
|
|
67
|
-
"@types/node": "^22.19.
|
|
68
|
-
"@types/react": "^19.2.
|
|
66
|
+
"@types/node": "^22.19.1",
|
|
67
|
+
"@types/react": "^19.2.4",
|
|
69
68
|
"typescript": "^5.9.3",
|
|
70
69
|
"vitest": "^4.0.8"
|
|
71
70
|
},
|
|
@@ -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
|
+
}
|
|
@@ -18,19 +18,19 @@ export default function Input<TFieldValues extends FieldValues>({
|
|
|
18
18
|
control: Control<TFieldValues>
|
|
19
19
|
type?: "text" | "date"
|
|
20
20
|
}) {
|
|
21
|
-
const
|
|
21
|
+
const errorMessage = useFieldError({ control, name })
|
|
22
22
|
return (
|
|
23
23
|
<div key={name} className="flex w-full flex-col">
|
|
24
24
|
<Label for={name} label={label} />
|
|
25
25
|
<input
|
|
26
|
-
className={`dy-input dy-input-bordered ${
|
|
26
|
+
className={`dy-input dy-input-bordered shadow-inner ${errorMessage ? "dy-input-error" : ""}`}
|
|
27
27
|
type={type}
|
|
28
28
|
id={name}
|
|
29
|
-
aria-invalid={
|
|
29
|
+
aria-invalid={!!errorMessage}
|
|
30
30
|
{...control.register(name)}
|
|
31
31
|
/>
|
|
32
|
-
<ErrorMessage
|
|
33
|
-
<Hint
|
|
32
|
+
<ErrorMessage message={errorMessage} />
|
|
33
|
+
<Hint label={hint} />
|
|
34
34
|
</div>
|
|
35
35
|
)
|
|
36
36
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Control, FieldValues, Path } from "react-hook-form"
|
|
2
2
|
|
|
3
|
-
import { useI18n } from "../../../i18n/react/useI18n"
|
|
4
3
|
import ErrorMessage from "./atoms/ErrorMessage"
|
|
5
4
|
import Hint from "./atoms/Hint"
|
|
6
5
|
import Label from "./atoms/Label"
|
|
@@ -17,27 +16,26 @@ export default function Select<TFieldValues extends FieldValues>({
|
|
|
17
16
|
label: string
|
|
18
17
|
hint?: string
|
|
19
18
|
control: Control<TFieldValues>
|
|
20
|
-
options: { id: string;
|
|
19
|
+
options: { id: string; labelText?: string }[]
|
|
21
20
|
}) {
|
|
22
|
-
const {
|
|
23
|
-
const hasError = !!useFieldError({ control, name })
|
|
21
|
+
const errorMessage = useFieldError({ control, name })
|
|
24
22
|
return (
|
|
25
23
|
<div key={name} className="flex w-full flex-col">
|
|
26
24
|
<Label for={name} label={label} />
|
|
27
25
|
<select
|
|
28
26
|
{...control.register(name)}
|
|
29
27
|
id={name}
|
|
30
|
-
aria-invalid={
|
|
31
|
-
className={`dy-select dy-select-bordered ${
|
|
28
|
+
aria-invalid={!!errorMessage}
|
|
29
|
+
className={`dy-select dy-select-bordered text-base shadow-sm ${errorMessage ? "dy-select-error" : ""}`}
|
|
32
30
|
>
|
|
33
|
-
{options.map(({ id,
|
|
31
|
+
{options.map(({ id, labelText }) => (
|
|
34
32
|
<option key={id} value={id}>
|
|
35
|
-
{
|
|
33
|
+
{labelText ?? id}
|
|
36
34
|
</option>
|
|
37
35
|
))}
|
|
38
36
|
</select>
|
|
39
|
-
<ErrorMessage
|
|
40
|
-
<Hint
|
|
37
|
+
<ErrorMessage message={errorMessage} />
|
|
38
|
+
<Hint label={hint} />
|
|
41
39
|
</div>
|
|
42
40
|
)
|
|
43
41
|
}
|
|
@@ -8,7 +8,7 @@ import type { MediaItem } from "../../types/media-item"
|
|
|
8
8
|
const SUCCESS_DURATION_MS = 2000
|
|
9
9
|
|
|
10
10
|
const baseButtonClass =
|
|
11
|
-
"flex min-w-52 items-center justify-center gap-2 rounded-2xl px-
|
|
11
|
+
"flex min-w-52 items-center justify-center gap-2 rounded-2xl px-4 py-3 font-bold uppercase shadow-sm transition-colors easy-in-out focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-900 disabled:cursor-not-allowed"
|
|
12
12
|
|
|
13
13
|
const buttonStateClasses = {
|
|
14
14
|
idle: "bg-gray-800 text-gray-100 hover:bg-gray-950 hover:text-gray-300 disabled:bg-gray-600 disabled:text-gray-200",
|
|
@@ -19,8 +19,10 @@ const buttonStateClasses = {
|
|
|
19
19
|
} as const
|
|
20
20
|
|
|
21
21
|
const buttonLabels = {
|
|
22
|
-
idle:
|
|
23
|
-
|
|
22
|
+
idle: import.meta.env.DEV
|
|
23
|
+
? "ln.admin.save-changes"
|
|
24
|
+
: "ln.admin.publish-changes",
|
|
25
|
+
success: import.meta.env.DEV ? "ln.admin.saved" : "ln.admin.published",
|
|
24
26
|
error: "ln.admin.failed",
|
|
25
27
|
} as const
|
|
26
28
|
|
|
@@ -38,10 +40,9 @@ export default function SubmitButton({
|
|
|
38
40
|
className?: string
|
|
39
41
|
}) {
|
|
40
42
|
const { t } = useI18n()
|
|
41
|
-
const {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
})
|
|
43
|
+
const { isSubmitting, isSubmitSuccessful, submitCount } = useFormState({
|
|
44
|
+
control,
|
|
45
|
+
})
|
|
45
46
|
|
|
46
47
|
const buttonState = useButtonState(isSubmitSuccessful, submitCount)
|
|
47
48
|
const buttonClass = `${baseButtonClass} ${buttonStateClasses[buttonState]} ${className}`
|
|
@@ -49,11 +50,7 @@ export default function SubmitButton({
|
|
|
49
50
|
const icon = icons[buttonState]
|
|
50
51
|
|
|
51
52
|
return (
|
|
52
|
-
<button
|
|
53
|
-
className={buttonClass}
|
|
54
|
-
type="submit"
|
|
55
|
-
disabled={!isValid || isSubmitting}
|
|
56
|
-
>
|
|
53
|
+
<button className={buttonClass} type="submit" disabled={isSubmitting}>
|
|
57
54
|
{icon && <Icon className={icon} ariaLabel="" />}
|
|
58
55
|
{t(label)}
|
|
59
56
|
</button>
|
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
import { ErrorMessage as RhfErrorMessage } from "@hookform/error-message"
|
|
2
|
-
import { type Control, useFormState } from "react-hook-form"
|
|
3
|
-
|
|
4
1
|
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
5
2
|
|
|
6
|
-
export default function ErrorMessage({
|
|
7
|
-
name,
|
|
8
|
-
control,
|
|
9
|
-
}: {
|
|
10
|
-
name: string
|
|
11
|
-
control: Control<any>
|
|
12
|
-
}) {
|
|
3
|
+
export default function ErrorMessage({ message }: { message?: string }) {
|
|
13
4
|
const { t } = useI18n()
|
|
14
|
-
|
|
5
|
+
if (!message) {
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
15
8
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
render={({ message }) => {
|
|
20
|
-
if (!message) {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
return (
|
|
24
|
-
<p
|
|
25
|
-
className="my-2 flex flex-col gap-1 text-sm text-rose-800"
|
|
26
|
-
role="alert"
|
|
27
|
-
>
|
|
28
|
-
{t(message)}
|
|
29
|
-
</p>
|
|
30
|
-
)
|
|
31
|
-
}}
|
|
32
|
-
/>
|
|
9
|
+
<p className="my-2 flex flex-col gap-1 text-sm text-rose-800" role="alert">
|
|
10
|
+
{t(message)}
|
|
11
|
+
</p>
|
|
33
12
|
)
|
|
34
13
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
2
|
|
|
3
|
-
export default function Hint({
|
|
3
|
+
export default function Hint({ label }: { label?: string }) {
|
|
4
4
|
const { t } = useI18n()
|
|
5
5
|
return (
|
|
6
6
|
<div className="flex h-12 w-full items-start justify-end p-2">
|
|
7
|
-
{
|
|
7
|
+
{label && <span className="dy-label-text-alt">{t(label)}</span>}
|
|
8
8
|
</div>
|
|
9
9
|
)
|
|
10
10
|
}
|
|
@@ -3,15 +3,19 @@ import { useI18n } from "../../../../i18n/react/useI18n"
|
|
|
3
3
|
export default function Label({
|
|
4
4
|
label,
|
|
5
5
|
for: htmlFor,
|
|
6
|
+
size = "sm",
|
|
7
|
+
className,
|
|
6
8
|
}: {
|
|
7
9
|
label: string
|
|
8
10
|
for: string
|
|
11
|
+
className?: string
|
|
12
|
+
size?: "sm" | "xs"
|
|
9
13
|
}) {
|
|
10
14
|
const { t } = useI18n()
|
|
11
15
|
return (
|
|
12
16
|
<label
|
|
13
17
|
htmlFor={htmlFor}
|
|
14
|
-
className="pb-2 text-sm
|
|
18
|
+
className={`font-bold uppercase text-gray-500 ${size === "sm" ? "pb-2 text-sm" : "pb-1 text-xs"} ${className}`}
|
|
15
19
|
>
|
|
16
20
|
{t(label)}
|
|
17
21
|
</label>
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { useI18n } from "../../../../i18n/react/useI18n"
|
|
2
2
|
|
|
3
|
-
export default function
|
|
3
|
+
export default function Legend({
|
|
4
|
+
label,
|
|
5
|
+
size = "sm",
|
|
6
|
+
className,
|
|
7
|
+
}: {
|
|
8
|
+
label: string
|
|
9
|
+
className?: string
|
|
10
|
+
size?: "sm" | "xs"
|
|
11
|
+
}) {
|
|
4
12
|
const { t } = useI18n()
|
|
5
13
|
return (
|
|
6
|
-
<legend
|
|
14
|
+
<legend
|
|
15
|
+
className={`pb-2 font-bold uppercase text-gray-500 ${size === "sm" ? "text-sm" : "text-xs"} ${className}`}
|
|
16
|
+
>
|
|
7
17
|
{t(label)}
|
|
8
18
|
</legend>
|
|
9
19
|
)
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import { type Control,
|
|
1
|
+
import { type Control, get, useFormState } from "react-hook-form"
|
|
2
2
|
|
|
3
3
|
export function useFieldError({
|
|
4
4
|
control,
|
|
5
5
|
name,
|
|
6
|
-
index,
|
|
7
6
|
}: {
|
|
8
7
|
control: Control<any>
|
|
9
8
|
name: string
|
|
10
|
-
index?: number
|
|
11
9
|
}) {
|
|
12
10
|
const { errors } = useFormState({ control, name, exact: true })
|
|
13
|
-
const error = errors
|
|
14
|
-
|
|
15
|
-
return undefined
|
|
16
|
-
}
|
|
17
|
-
if (Array.isArray(error)) {
|
|
18
|
-
return index !== undefined ? (error[index] as FieldError) : undefined
|
|
19
|
-
}
|
|
20
|
-
return error as FieldError
|
|
11
|
+
const error = get(errors, name) as { message: string } | undefined
|
|
12
|
+
return error?.message
|
|
21
13
|
}
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
ln.admin.edit: Edit
|
|
2
|
-
ln.admin.
|
|
2
|
+
ln.admin.publish-changes: Publish Changes
|
|
3
|
+
ln.admin.published: Published
|
|
4
|
+
ln.admin.save-changes: Save Changes
|
|
3
5
|
ln.admin.saved: Saved
|
|
4
6
|
ln.admin.failed: Failed
|
|
5
7
|
ln.admin.remove: Remove
|
|
8
|
+
ln.admin.name: Name
|
|
6
9
|
ln.admin.add-author: Add Author
|
|
10
|
+
ln.admin.add-category: Add Category
|
|
11
|
+
ln.admin.collections: Collections
|
|
12
|
+
ln.admin.add-collection: Add Collection
|
|
7
13
|
ln.admin.edit-media-item: Edit media item
|
|
14
|
+
ln.admin.position-in-collection: Position in Collection
|
|
8
15
|
ln.admin.back-to-details-page: Back to details page
|
|
9
16
|
ln.admin.title: Title
|
|
10
17
|
ln.admin.common-id: Common ID
|
|
11
18
|
ln.admin.authors: Authors
|
|
12
19
|
ln.admin.created-on: Created on
|
|
13
20
|
ln.admin.created-on-hint: When has this item been created on this library?
|
|
14
|
-
ln.admin.common-id-hint: The
|
|
15
|
-
ln.admin.errors.non-empty-string:
|
|
16
|
-
ln.admin.errors.invalid-date:
|
|
17
|
-
ln.admin.errors.required:
|
|
21
|
+
ln.admin.common-id-hint: The English title, all lowercase, words separated with hyphens.
|
|
22
|
+
ln.admin.errors.non-empty-string: Please enter at least one character.
|
|
23
|
+
ln.admin.errors.invalid-date: That date doesn't look right.
|
|
24
|
+
ln.admin.errors.required: This field is required.
|
|
25
|
+
ln.admin.errors.gte-0: Use a number zero or greater.
|
|
26
|
+
ln.admin.errors.unique-elements: Please choose a different value for each entry.
|
|
27
|
+
ln.admin.errors.integer: Please enter a whole number.
|
|
@@ -11,24 +11,33 @@ import Select from "../../components/form/Select"
|
|
|
11
11
|
import SubmitButton from "../../components/form/SubmitButton"
|
|
12
12
|
import { type MediaItem, mediaItemSchema } from "../../types/media-item"
|
|
13
13
|
import Authors from "./fields/Authors"
|
|
14
|
+
import Categories from "./fields/Categories"
|
|
15
|
+
import Collections from "./fields/Collections"
|
|
14
16
|
import { updateMediaItem } from "./media-item-store"
|
|
15
17
|
|
|
18
|
+
type SelectOption = { id: string; labelText: string }
|
|
19
|
+
|
|
16
20
|
export default function EditForm({
|
|
17
21
|
mediaId,
|
|
18
22
|
mediaItem,
|
|
19
23
|
i18nConfig,
|
|
20
24
|
mediaTypes,
|
|
21
25
|
languages,
|
|
26
|
+
categories,
|
|
27
|
+
collections,
|
|
22
28
|
}: {
|
|
23
29
|
mediaId: string
|
|
24
30
|
mediaItem: MediaItem
|
|
25
31
|
i18nConfig: I18nConfig
|
|
26
|
-
mediaTypes:
|
|
27
|
-
languages:
|
|
32
|
+
mediaTypes: SelectOption[]
|
|
33
|
+
languages: SelectOption[]
|
|
34
|
+
categories: SelectOption[]
|
|
35
|
+
collections: SelectOption[]
|
|
28
36
|
}) {
|
|
29
37
|
const { handleSubmit, control } = useForm({
|
|
30
38
|
defaultValues: mediaItem,
|
|
31
39
|
mode: "onTouched",
|
|
40
|
+
shouldFocusError: true,
|
|
32
41
|
resolver: zodResolver(mediaItemSchema),
|
|
33
42
|
})
|
|
34
43
|
const onSubmit = handleSubmit(
|
|
@@ -37,7 +46,12 @@ export default function EditForm({
|
|
|
37
46
|
const i18n = createI18n(i18nConfig)
|
|
38
47
|
return (
|
|
39
48
|
<I18nContext.Provider value={i18n}>
|
|
40
|
-
<form onSubmit={onSubmit}>
|
|
49
|
+
<form className="flex flex-col" onSubmit={onSubmit}>
|
|
50
|
+
<div className="mb-8 flex items-end justify-between">
|
|
51
|
+
<h1 className="text-3xl">{i18n.t("ln.admin.edit-media-item")}</h1>
|
|
52
|
+
<SubmitButton control={control} />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
41
55
|
<Input name="title" label="ln.admin.title" control={control} />
|
|
42
56
|
<Input
|
|
43
57
|
name="commonId"
|
|
@@ -65,8 +79,10 @@ export default function EditForm({
|
|
|
65
79
|
type="date"
|
|
66
80
|
control={control}
|
|
67
81
|
/>
|
|
82
|
+
<Categories categories={categories} control={control} />
|
|
83
|
+
<Collections collections={collections} control={control} />
|
|
68
84
|
|
|
69
|
-
<SubmitButton className="
|
|
85
|
+
<SubmitButton className="self-end" control={control} />
|
|
70
86
|
</form>
|
|
71
87
|
</I18nContext.Provider>
|
|
72
88
|
)
|