lightnet 3.10.7 → 3.11.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/CHANGELOG.md +25 -0
- package/__e2e__/admin.spec.ts +54 -20
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/package.json +5 -5
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +23 -3
- package/package.json +10 -10
- package/src/admin/components/form/DynamicArray.tsx +82 -30
- package/src/admin/components/form/Input.tsx +17 -3
- package/src/admin/components/form/LazyLoadedMarkdownEditor.tsx +2 -2
- package/src/admin/components/form/MarkdownEditor.tsx +12 -4
- package/src/admin/components/form/Select.tsx +25 -13
- package/src/admin/components/form/SubmitButton.tsx +3 -3
- package/src/admin/components/form/atoms/Button.tsx +27 -0
- package/src/admin/components/form/atoms/ErrorMessage.tsx +1 -1
- package/src/admin/components/form/atoms/FileUpload.tsx +190 -0
- package/src/admin/components/form/atoms/Hint.tsx +3 -3
- package/src/admin/components/form/atoms/Label.tsx +18 -7
- package/src/admin/components/form/hooks/use-field-error.tsx +23 -2
- package/src/admin/components/form/utils/get-border-class.ts +22 -0
- package/src/admin/i18n/admin-i18n.ts +21 -0
- package/src/admin/i18n/translations/en.yml +24 -2
- package/src/admin/pages/AdminRoute.astro +1 -3
- package/src/admin/pages/media/EditForm.tsx +35 -11
- package/src/admin/pages/media/EditRoute.astro +33 -17
- package/src/admin/pages/media/fields/Authors.tsx +15 -5
- package/src/admin/pages/media/fields/Categories.tsx +17 -5
- package/src/admin/pages/media/fields/Collections.tsx +21 -11
- package/src/admin/pages/media/fields/Content.tsx +150 -0
- package/src/admin/pages/media/fields/Image.tsx +119 -0
- package/src/admin/pages/media/file-system.ts +6 -2
- package/src/admin/pages/media/media-item-store.ts +46 -3
- package/src/admin/types/media-item.ts +29 -0
- package/src/astro-integration/config.ts +10 -0
- package/src/astro-integration/integration.ts +7 -3
- package/src/components/SearchInput.astro +3 -3
- package/src/content/get-media-items.ts +2 -1
- package/src/i18n/react/i18n-context.ts +16 -5
- package/src/i18n/react/prepare-i18n-config.ts +1 -1
- package/src/i18n/react/{useI18n.ts → use-i18n.ts} +1 -1
- package/src/i18n/translations/TRANSLATION-STATUS.md +4 -0
- package/src/i18n/translations/ur.yml +25 -0
- package/src/i18n/translations.ts +1 -0
- package/src/layouts/Page.astro +5 -6
- package/src/layouts/components/LanguagePicker.astro +11 -5
- package/src/layouts/components/Menu.astro +76 -10
- package/src/pages/404Route.astro +3 -1
- package/src/pages/details-page/components/main-details/EditButton.astro +1 -1
- package/src/pages/details-page/utils/create-content-metadata.ts +2 -1
- package/src/pages/search-page/components/LoadingSkeleton.tsx +21 -14
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.tsx +33 -29
- package/src/pages/search-page/components/SearchListItem.tsx +1 -1
- package/src/pages/search-page/components/Select.tsx +15 -13
- package/src/layouts/components/PreloadReact.tsx +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# lightnet
|
|
2
2
|
|
|
3
|
+
## 3.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#347](https://github.com/LightNetDev/LightNet/pull/347) [`17ea3d9`](https://github.com/LightNetDev/LightNet/commit/17ea3d9b599d9456b36e1974d3a8f5dc7d39fb65) Thanks [@ajjn](https://github.com/ajjn)! - Add translation for Urdu
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#344](https://github.com/LightNetDev/LightNet/pull/344) [`a48d2a7`](https://github.com/LightNetDev/LightNet/commit/a48d2a7ec9b782c763950b02cdbe420b1ed000ec) Thanks [@smn-cds](https://github.com/smn-cds)! - Remove preload React logic
|
|
12
|
+
|
|
13
|
+
## 3.10.8
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#340](https://github.com/LightNetDev/LightNet/pull/340) [`6162179`](https://github.com/LightNetDev/LightNet/commit/6162179b90e987359934bd08ac9d4500eb560e25) Thanks [@smn-cds](https://github.com/smn-cds)! - Tighten locale detection to avoid partial matches
|
|
18
|
+
|
|
19
|
+
When the URL path starts with a locale-like prefix, only full segment matches should be treated as a locale. Previously `/enx` was parsed as locale `en`; now only `/en/...` (or just `/en`) will match.
|
|
20
|
+
|
|
21
|
+
- [#346](https://github.com/LightNetDev/LightNet/pull/346) [`2bc1560`](https://github.com/LightNetDev/LightNet/commit/2bc15600b0a9bcfc3410f155191b7ea1c9c5306a) Thanks [@smn-cds](https://github.com/smn-cds)! - Fixes an issue where top menus would not close when tapping outside the menu on iOS Safari.
|
|
22
|
+
This update also improves overall menu accessibility.
|
|
23
|
+
|
|
24
|
+
- [#345](https://github.com/LightNetDev/LightNet/pull/345) [`1e6cddb`](https://github.com/LightNetDev/LightNet/commit/1e6cddba0e783f9d59da7d36dd154dbcd784ed6b) Thanks [@smn-cds](https://github.com/smn-cds)! - Fix menu not working on ios safari
|
|
25
|
+
|
|
26
|
+
- [#340](https://github.com/LightNetDev/LightNet/pull/340) [`6162179`](https://github.com/LightNetDev/LightNet/commit/6162179b90e987359934bd08ac9d4500eb560e25) Thanks [@smn-cds](https://github.com/smn-cds)! - Update dependencies
|
|
27
|
+
|
|
3
28
|
## 3.10.7
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/__e2e__/admin.spec.ts
CHANGED
|
@@ -25,13 +25,13 @@ test.describe("Edit button on details page", () => {
|
|
|
25
25
|
await expect(editButton).toBeHidden()
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
test("Should show `Edit` button on book details page after visiting `/
|
|
28
|
+
test("Should show `Edit` button on book details page after visiting `/admin/` path.", async ({
|
|
29
29
|
page,
|
|
30
30
|
lightnet,
|
|
31
31
|
}) => {
|
|
32
32
|
const ln = await lightnet()
|
|
33
33
|
|
|
34
|
-
await page.goto(ln.resolveURL("/
|
|
34
|
+
await page.goto(ln.resolveURL("/admin/"))
|
|
35
35
|
await expect(
|
|
36
36
|
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
37
37
|
).toBeVisible()
|
|
@@ -45,15 +45,15 @@ test.describe("Edit button on details page", () => {
|
|
|
45
45
|
await expect(editButton).toBeVisible()
|
|
46
46
|
await expect(editButton).toHaveAttribute(
|
|
47
47
|
"href",
|
|
48
|
-
"/
|
|
48
|
+
"/admin/media/faithful-freestyle--en",
|
|
49
49
|
)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test("Should show `Edit` button on video details page after visiting `/
|
|
52
|
+
test("Should show `Edit` button on video details page after visiting `/admin/` path.", async ({
|
|
53
53
|
page,
|
|
54
54
|
lightnet,
|
|
55
55
|
}) => {
|
|
56
|
-
const ln = await lightnet("/
|
|
56
|
+
const ln = await lightnet("/admin/")
|
|
57
57
|
|
|
58
58
|
await expect(
|
|
59
59
|
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
@@ -68,15 +68,15 @@ test.describe("Edit button on details page", () => {
|
|
|
68
68
|
await expect(editButton).toBeVisible()
|
|
69
69
|
await expect(editButton).toHaveAttribute(
|
|
70
70
|
"href",
|
|
71
|
-
"/
|
|
71
|
+
"/admin/media/how-to-kickflip--de",
|
|
72
72
|
)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
test("Should show `Edit` button on audio details page after visiting `/
|
|
75
|
+
test("Should show `Edit` button on audio details page after visiting `/admin/` path.", async ({
|
|
76
76
|
page,
|
|
77
77
|
lightnet,
|
|
78
78
|
}) => {
|
|
79
|
-
const ln = await lightnet("/
|
|
79
|
+
const ln = await lightnet("/admin/")
|
|
80
80
|
|
|
81
81
|
await expect(
|
|
82
82
|
page.getByText("Admin features are enabled now.", { exact: true }),
|
|
@@ -91,7 +91,7 @@ test.describe("Edit button on details page", () => {
|
|
|
91
91
|
await expect(editButton).toBeVisible()
|
|
92
92
|
await expect(editButton).toHaveAttribute(
|
|
93
93
|
"href",
|
|
94
|
-
"/
|
|
94
|
+
"/admin/media/skate-sounds--en",
|
|
95
95
|
)
|
|
96
96
|
})
|
|
97
97
|
|
|
@@ -99,7 +99,7 @@ test.describe("Edit button on details page", () => {
|
|
|
99
99
|
page,
|
|
100
100
|
lightnet,
|
|
101
101
|
}) => {
|
|
102
|
-
const ln = await lightnet("/
|
|
102
|
+
const ln = await lightnet("/admin/")
|
|
103
103
|
await ln.goto("/en/media/faithful-freestyle--en")
|
|
104
104
|
|
|
105
105
|
const editButton = page.locator("#edit-btn")
|
|
@@ -107,7 +107,7 @@ test.describe("Edit button on details page", () => {
|
|
|
107
107
|
|
|
108
108
|
await editButton.click()
|
|
109
109
|
await expect(page).toHaveURL(
|
|
110
|
-
ln.resolveURL("/
|
|
110
|
+
ln.resolveURL("/admin/media/faithful-freestyle--en"),
|
|
111
111
|
)
|
|
112
112
|
await expect(
|
|
113
113
|
page.getByText("Edit media item", { exact: false }),
|
|
@@ -139,7 +139,7 @@ test.describe("Media item edit page", () => {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
const getPublishButton = (page: Page) =>
|
|
142
|
-
page.getByRole("button", { name: "Publish
|
|
142
|
+
page.getByRole("button", { name: "Publish changes" }).first()
|
|
143
143
|
|
|
144
144
|
const expectPublishedMessage = (page: Page) =>
|
|
145
145
|
expect(
|
|
@@ -147,7 +147,7 @@ test.describe("Media item edit page", () => {
|
|
|
147
147
|
).toBeVisible()
|
|
148
148
|
|
|
149
149
|
test("should edit title", async ({ page, lightnet }) => {
|
|
150
|
-
await lightnet("/
|
|
150
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
151
151
|
const writeFileRequest = await recordWriteFile(page)
|
|
152
152
|
|
|
153
153
|
const updatedTitle = "Faithful Freestyle (Edited)"
|
|
@@ -175,7 +175,7 @@ test.describe("Media item edit page", () => {
|
|
|
175
175
|
})
|
|
176
176
|
|
|
177
177
|
test("Should update media type", async ({ page, lightnet }) => {
|
|
178
|
-
await lightnet("/
|
|
178
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
179
179
|
const writeFileRequest = await recordWriteFile(page)
|
|
180
180
|
|
|
181
181
|
const typeSelect = page.getByLabel("Type").first()
|
|
@@ -198,7 +198,7 @@ test.describe("Media item edit page", () => {
|
|
|
198
198
|
})
|
|
199
199
|
|
|
200
200
|
test("Should update author name", async ({ page, lightnet }) => {
|
|
201
|
-
await lightnet("/
|
|
201
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
202
202
|
const writeFileRequest = await recordWriteFile(page)
|
|
203
203
|
|
|
204
204
|
const authorsFieldset = page.getByRole("group", { name: "Authors" })
|
|
@@ -223,7 +223,7 @@ test.describe("Media item edit page", () => {
|
|
|
223
223
|
})
|
|
224
224
|
|
|
225
225
|
test("Should add author", async ({ page, lightnet }) => {
|
|
226
|
-
await lightnet("/
|
|
226
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
227
227
|
const writeFileRequest = await recordWriteFile(page)
|
|
228
228
|
|
|
229
229
|
const authorsFieldset = page.getByRole("group", { name: "Authors" })
|
|
@@ -249,7 +249,7 @@ test.describe("Media item edit page", () => {
|
|
|
249
249
|
})
|
|
250
250
|
|
|
251
251
|
test("Should remove author", async ({ page, lightnet }) => {
|
|
252
|
-
await lightnet("/
|
|
252
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
253
253
|
const writeFileRequest = await recordWriteFile(page)
|
|
254
254
|
|
|
255
255
|
const authorsFieldset = page.getByRole("group", { name: "Authors" })
|
|
@@ -283,7 +283,7 @@ test.describe("Media item edit page", () => {
|
|
|
283
283
|
page,
|
|
284
284
|
lightnet,
|
|
285
285
|
}) => {
|
|
286
|
-
await lightnet("/
|
|
286
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
287
287
|
|
|
288
288
|
const commonIdInput = page.getByLabel("Common ID")
|
|
289
289
|
await expect(commonIdInput).toHaveValue("faithful-freestyle")
|
|
@@ -302,7 +302,7 @@ test.describe("Media item edit page", () => {
|
|
|
302
302
|
page,
|
|
303
303
|
lightnet,
|
|
304
304
|
}) => {
|
|
305
|
-
await lightnet("/
|
|
305
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
306
306
|
|
|
307
307
|
const categoriesFieldset = page.getByRole("group", { name: "Categories" })
|
|
308
308
|
await page.getByRole("button", { name: "Add Category" }).click()
|
|
@@ -325,7 +325,7 @@ test.describe("Media item edit page", () => {
|
|
|
325
325
|
page,
|
|
326
326
|
lightnet,
|
|
327
327
|
}) => {
|
|
328
|
-
await lightnet("/
|
|
328
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
329
329
|
|
|
330
330
|
const categoriesFieldset = page.getByRole("group", { name: "Categories" })
|
|
331
331
|
await page.getByRole("button", { name: "Add Category" }).click()
|
|
@@ -347,4 +347,38 @@ test.describe("Media item edit page", () => {
|
|
|
347
347
|
"true",
|
|
348
348
|
)
|
|
349
349
|
})
|
|
350
|
+
|
|
351
|
+
test("should not submit the form on enter", async ({ page, lightnet }) => {
|
|
352
|
+
await lightnet("/admin/media/faithful-freestyle--en")
|
|
353
|
+
const writeFileRequest = await recordWriteFile(page)
|
|
354
|
+
const writePromise = writeFileRequest()
|
|
355
|
+
|
|
356
|
+
const updatedTitle = "Faithful Freestyle (Enter key)"
|
|
357
|
+
const titleInput = page.getByLabel("Title")
|
|
358
|
+
await titleInput.fill(updatedTitle)
|
|
359
|
+
|
|
360
|
+
const publishButton = getPublishButton(page)
|
|
361
|
+
await expect(publishButton).toBeEnabled()
|
|
362
|
+
|
|
363
|
+
await titleInput.press("Enter")
|
|
364
|
+
|
|
365
|
+
const enterResult = await Promise.race([
|
|
366
|
+
writePromise.then(() => "submitted"),
|
|
367
|
+
page.waitForTimeout(500).then(() => "timeout"),
|
|
368
|
+
])
|
|
369
|
+
expect(enterResult).toBe("timeout")
|
|
370
|
+
await expect(publishButton).toBeEnabled()
|
|
371
|
+
|
|
372
|
+
await publishButton.click()
|
|
373
|
+
|
|
374
|
+
const expectedMediaItem = JSON.parse(
|
|
375
|
+
await readFile(faithfulFreestyleMediaUrl, "utf-8"),
|
|
376
|
+
)
|
|
377
|
+
const { body } = await writePromise
|
|
378
|
+
expect(body).toEqual({
|
|
379
|
+
...expectedMediaItem,
|
|
380
|
+
title: updatedTitle,
|
|
381
|
+
})
|
|
382
|
+
await expectPublishedMessage(page)
|
|
383
|
+
})
|
|
350
384
|
})
|
|
@@ -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.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/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.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/node_modules/astro/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/astro@5.16.6_@types+node@25.0.3_jiti@2.4.2_lightningcss@1.29.1_rollup@4.54.0_terser@5.39.0_typescript@5.9.3_yaml@2.8.2/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" "$@"
|
|
@@ -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/tailwindcss@3.4.
|
|
13
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/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/tailwindcss@3.4.
|
|
15
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/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/../tailwindcss/lib/cli.js" "$@"
|
|
@@ -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/tailwindcss@3.4.
|
|
13
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/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/tailwindcss@3.4.
|
|
15
|
+
export NODE_PATH="/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/lib/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/node_modules/tailwindcss/node_modules:/home/runner/work/LightNet/LightNet/node_modules/.pnpm/tailwindcss@3.4.19_yaml@2.8.2/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/../tailwindcss/lib/cli.js" "$@"
|
|
@@ -7,12 +7,12 @@
|
|
|
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.
|
|
11
|
-
"lightnet": "^3.
|
|
12
|
-
"react": "^19.2.
|
|
13
|
-
"react-dom": "^19.2.
|
|
10
|
+
"astro": "^5.16.6",
|
|
11
|
+
"lightnet": "^3.11.0",
|
|
12
|
+
"react": "^19.2.3",
|
|
13
|
+
"react-dom": "^19.2.3",
|
|
14
14
|
"sharp": "^0.34.5",
|
|
15
|
-
"tailwindcss": "^3.4.
|
|
15
|
+
"tailwindcss": "^3.4.19",
|
|
16
16
|
"typescript": "^5.9.3"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
@@ -102,9 +102,29 @@ test("Should create complete content metadata", () => {
|
|
|
102
102
|
type: "package",
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
{
|
|
106
|
+
url: "/some.zip",
|
|
107
|
+
label: "foo",
|
|
108
|
+
expected: {
|
|
109
|
+
label: "foo",
|
|
110
|
+
isExternal: false,
|
|
111
|
+
extension: "zip",
|
|
112
|
+
type: "package",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
url: "/some.zip",
|
|
117
|
+
label: "",
|
|
118
|
+
expected: {
|
|
119
|
+
label: "some",
|
|
120
|
+
isExternal: false,
|
|
121
|
+
extension: "zip",
|
|
122
|
+
type: "package",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
].forEach(({ url, expected, label }) => {
|
|
126
|
+
test(`Should create content metadata for url '${url}' ${label !== undefined ? `and label '${label}'` : ""}`, () => {
|
|
127
|
+
expect(createContentMetadata({ url, label })).toMatchObject(expected)
|
|
108
128
|
})
|
|
109
129
|
})
|
|
110
130
|
|
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.
|
|
6
|
+
"version": "3.11.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LightNetDev/lightnet",
|
|
@@ -50,24 +50,24 @@
|
|
|
50
50
|
"@hookform/resolvers": "^5.2.2",
|
|
51
51
|
"@iconify-json/mdi": "^1.2.3",
|
|
52
52
|
"@iconify/tailwind": "^1.2.0",
|
|
53
|
-
"@mdxeditor/editor": "^3.
|
|
53
|
+
"@mdxeditor/editor": "^3.52.1",
|
|
54
54
|
"@tailwindcss/typography": "^0.5.19",
|
|
55
|
-
"@tanstack/react-virtual": "^3.13.
|
|
55
|
+
"@tanstack/react-virtual": "^3.13.13",
|
|
56
56
|
"daisyui": "^4.12.24",
|
|
57
57
|
"embla-carousel": "^8.6.0",
|
|
58
58
|
"embla-carousel-wheel-gestures": "^8.1.0",
|
|
59
59
|
"fuse.js": "^7.1.0",
|
|
60
|
-
"i18next": "^25.
|
|
60
|
+
"i18next": "^25.7.3",
|
|
61
61
|
"marked": "^16.4.2",
|
|
62
|
-
"react-hook-form": "^7.
|
|
63
|
-
"yaml": "^2.8.
|
|
62
|
+
"react-hook-form": "^7.69.0",
|
|
63
|
+
"yaml": "^2.8.2"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@playwright/test": "^1.
|
|
67
|
-
"@types/node": "^22.19.
|
|
68
|
-
"@types/react": "^19.2.
|
|
66
|
+
"@playwright/test": "^1.57.0",
|
|
67
|
+
"@types/node": "^22.19.3",
|
|
68
|
+
"@types/react": "^19.2.7",
|
|
69
69
|
"typescript": "^5.9.3",
|
|
70
|
-
"vitest": "^4.0.
|
|
70
|
+
"vitest": "^4.0.16"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
|
73
73
|
"node": ">=22"
|
|
@@ -8,70 +8,122 @@ import {
|
|
|
8
8
|
} from "react-hook-form"
|
|
9
9
|
|
|
10
10
|
import Icon from "../../../components/Icon"
|
|
11
|
-
import { useI18n } from "../../../i18n/react/
|
|
11
|
+
import { useI18n } from "../../../i18n/react/use-i18n"
|
|
12
12
|
import ErrorMessage from "./atoms/ErrorMessage"
|
|
13
13
|
import Hint from "./atoms/Hint"
|
|
14
14
|
import Label from "./atoms/Label"
|
|
15
|
+
import { useFieldDirty } from "./hooks/use-field-dirty"
|
|
15
16
|
import { useFieldError } from "./hooks/use-field-error"
|
|
17
|
+
import { getBorderClass } from "./utils/get-border-class"
|
|
16
18
|
|
|
17
19
|
export default function DynamicArray<TFieldValues extends FieldValues>({
|
|
18
20
|
control,
|
|
19
21
|
name,
|
|
20
22
|
label,
|
|
23
|
+
required = false,
|
|
21
24
|
hint,
|
|
22
25
|
renderElement,
|
|
23
|
-
|
|
26
|
+
renderElementMeta,
|
|
27
|
+
renderAddButton,
|
|
24
28
|
}: {
|
|
25
29
|
name: ArrayPath<TFieldValues>
|
|
30
|
+
required?: boolean
|
|
26
31
|
label: string
|
|
27
32
|
hint?: string
|
|
28
33
|
control: Control<TFieldValues>
|
|
29
34
|
renderElement: (index: number) => ReactNode
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
) => void
|
|
36
|
-
}
|
|
35
|
+
renderElementMeta?: (index: number) => ReactNode
|
|
36
|
+
renderAddButton: (args: {
|
|
37
|
+
addElement: UseFieldArrayAppend<TFieldValues, ArrayPath<TFieldValues>>
|
|
38
|
+
index: number
|
|
39
|
+
}) => ReactNode
|
|
37
40
|
}) {
|
|
38
|
-
const { fields, append, remove } = useFieldArray({
|
|
41
|
+
const { fields, append, remove, swap } = useFieldArray({
|
|
39
42
|
name,
|
|
40
43
|
control,
|
|
41
44
|
})
|
|
42
|
-
const { t } = useI18n()
|
|
43
45
|
const errorMessage = useFieldError({ control, name })
|
|
46
|
+
const isDirty = useFieldDirty({ control, name })
|
|
44
47
|
return (
|
|
45
48
|
<fieldset key={name}>
|
|
46
49
|
<legend>
|
|
47
|
-
<Label
|
|
50
|
+
<Label
|
|
51
|
+
required={required}
|
|
52
|
+
label={label}
|
|
53
|
+
isDirty={isDirty}
|
|
54
|
+
isInvalid={!!errorMessage}
|
|
55
|
+
/>
|
|
48
56
|
</legend>
|
|
49
57
|
|
|
50
|
-
<div
|
|
58
|
+
<div
|
|
59
|
+
className={`flex w-full flex-col gap-1 rounded-xl rounded-ss-none ${getBorderClass({ isDirty, errorMessage })} bg-slate-200 p-1 shadow-inner`}
|
|
60
|
+
>
|
|
51
61
|
{fields.map((field, index) => (
|
|
52
|
-
<div
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
<div
|
|
63
|
+
className="w-full gap-2 rounded-xl bg-slate-50 px-2 pb-4 shadow-sm"
|
|
64
|
+
key={field.id}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
className={`flex items-center ${renderElementMeta ? "justify-between" : "justify-end"}`}
|
|
58
68
|
>
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
{renderElementMeta && renderElementMeta(index)}
|
|
70
|
+
<div className="-me-2 flex">
|
|
71
|
+
<ItemActionButton
|
|
72
|
+
icon="mdi--arrow-up"
|
|
73
|
+
label="ln.admin.move-up"
|
|
74
|
+
disabled={index === 0}
|
|
75
|
+
onClick={() => swap(index, index - 1)}
|
|
76
|
+
/>
|
|
77
|
+
<ItemActionButton
|
|
78
|
+
icon="mdi--arrow-down"
|
|
79
|
+
label="ln.admin.move-down"
|
|
80
|
+
disabled={index === fields.length - 1}
|
|
81
|
+
onClick={() => swap(index, index + 1)}
|
|
82
|
+
/>
|
|
83
|
+
<ItemActionButton
|
|
84
|
+
icon="mdi--remove"
|
|
85
|
+
label="ln.admin.remove"
|
|
86
|
+
onClick={() => remove(index)}
|
|
87
|
+
className="hover:!text-rose-800"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{renderElement(index)}
|
|
61
93
|
</div>
|
|
62
94
|
))}
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
onClick={() => {
|
|
67
|
-
addButton.onClick(append, fields.length)
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
{t(addButton.label)}
|
|
71
|
-
</button>
|
|
95
|
+
<div className="flex flex-col items-center py-2">
|
|
96
|
+
{renderAddButton({ addElement: append, index: fields.length })}
|
|
97
|
+
</div>
|
|
72
98
|
</div>
|
|
73
99
|
<ErrorMessage message={errorMessage} />
|
|
74
100
|
<Hint preserveSpace={true} label={hint} />
|
|
75
101
|
</fieldset>
|
|
76
102
|
)
|
|
77
103
|
}
|
|
104
|
+
|
|
105
|
+
function ItemActionButton({
|
|
106
|
+
label,
|
|
107
|
+
icon,
|
|
108
|
+
onClick,
|
|
109
|
+
disabled = false,
|
|
110
|
+
className,
|
|
111
|
+
}: {
|
|
112
|
+
label: string
|
|
113
|
+
icon: string
|
|
114
|
+
disabled?: boolean
|
|
115
|
+
onClick: () => void
|
|
116
|
+
className?: string
|
|
117
|
+
}) {
|
|
118
|
+
const { t } = useI18n()
|
|
119
|
+
return (
|
|
120
|
+
<button
|
|
121
|
+
className={`flex items-center rounded-xl p-2 text-slate-600 transition-colors ease-in-out hover:text-sky-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-700 disabled:text-slate-300 ${className}`}
|
|
122
|
+
type="button"
|
|
123
|
+
disabled={disabled}
|
|
124
|
+
onClick={onClick}
|
|
125
|
+
>
|
|
126
|
+
<Icon className={icon} ariaLabel={t(label)} />
|
|
127
|
+
</button>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import type { InputHTMLAttributes } from "react"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Control,
|
|
4
|
+
type FieldValues,
|
|
5
|
+
type Path,
|
|
6
|
+
type RegisterOptions,
|
|
7
|
+
} from "react-hook-form"
|
|
3
8
|
|
|
4
9
|
import ErrorMessage from "./atoms/ErrorMessage"
|
|
5
10
|
import Hint from "./atoms/Hint"
|
|
6
11
|
import Label from "./atoms/Label"
|
|
7
12
|
import { useFieldDirty } from "./hooks/use-field-dirty"
|
|
8
13
|
import { useFieldError } from "./hooks/use-field-error"
|
|
14
|
+
import { getBorderClass } from "./utils/get-border-class"
|
|
9
15
|
|
|
10
16
|
type Props<TFieldValues extends FieldValues> = {
|
|
11
17
|
name: Path<TFieldValues>
|
|
18
|
+
required?: boolean
|
|
12
19
|
label?: string
|
|
13
20
|
labelSize?: "small" | "medium"
|
|
14
21
|
hint?: string
|
|
15
22
|
preserveHintSpace?: boolean
|
|
16
23
|
control: Control<TFieldValues>
|
|
24
|
+
registerOptions?: RegisterOptions<TFieldValues>
|
|
17
25
|
} & InputHTMLAttributes<HTMLInputElement>
|
|
18
26
|
|
|
19
27
|
export default function Input<TFieldValues extends FieldValues>({
|
|
@@ -23,10 +31,14 @@ export default function Input<TFieldValues extends FieldValues>({
|
|
|
23
31
|
hint,
|
|
24
32
|
preserveHintSpace = true,
|
|
25
33
|
control,
|
|
34
|
+
className,
|
|
35
|
+
required = false,
|
|
36
|
+
registerOptions,
|
|
26
37
|
...inputProps
|
|
27
38
|
}: Props<TFieldValues>) {
|
|
28
39
|
const isDirty = useFieldDirty({ control, name })
|
|
29
40
|
const errorMessage = useFieldError({ control, name })
|
|
41
|
+
|
|
30
42
|
return (
|
|
31
43
|
<div key={name} className="group flex w-full flex-col">
|
|
32
44
|
{label && (
|
|
@@ -36,15 +48,17 @@ export default function Input<TFieldValues extends FieldValues>({
|
|
|
36
48
|
size={labelSize}
|
|
37
49
|
isDirty={isDirty}
|
|
38
50
|
isInvalid={!!errorMessage}
|
|
51
|
+
required={required}
|
|
39
52
|
/>
|
|
40
53
|
</label>
|
|
41
54
|
)}
|
|
42
55
|
|
|
43
56
|
<input
|
|
44
|
-
className={`
|
|
57
|
+
className={`rounded-xl ${getBorderClass({ isDirty, errorMessage })} px-4 py-3 shadow-inner disabled:text-slate-500 ${label ? "rounded-ss-none" : ""} ${className}`}
|
|
45
58
|
id={name}
|
|
46
59
|
aria-invalid={!!errorMessage}
|
|
47
|
-
{
|
|
60
|
+
aria-required={required}
|
|
61
|
+
{...control.register(name, registerOptions)}
|
|
48
62
|
{...inputProps}
|
|
49
63
|
/>
|
|
50
64
|
<ErrorMessage message={errorMessage} />
|
|
@@ -47,7 +47,7 @@ export default function LazyLoadedMarkdownEditor<
|
|
|
47
47
|
markdown={value ?? ""}
|
|
48
48
|
onBlur={onBlur}
|
|
49
49
|
onChange={onChange}
|
|
50
|
-
contentEditableClassName="prose bg-
|
|
50
|
+
contentEditableClassName="prose bg-slate-50 h-80 w-full max-w-full overflow-y-auto"
|
|
51
51
|
ref={ref}
|
|
52
52
|
onError={(error) =>
|
|
53
53
|
console.error("Error while editing markdown", error)
|
|
@@ -78,7 +78,7 @@ export default function LazyLoadedMarkdownEditor<
|
|
|
78
78
|
<CreateLink />
|
|
79
79
|
</>
|
|
80
80
|
),
|
|
81
|
-
toolbarClassName: "!rounded-none",
|
|
81
|
+
toolbarClassName: "!rounded-none !bg-slate-200",
|
|
82
82
|
}),
|
|
83
83
|
]}
|
|
84
84
|
/>
|
|
@@ -6,6 +6,7 @@ import Hint from "./atoms/Hint"
|
|
|
6
6
|
import Label from "./atoms/Label"
|
|
7
7
|
import { useFieldDirty } from "./hooks/use-field-dirty"
|
|
8
8
|
import { useFieldError } from "./hooks/use-field-error"
|
|
9
|
+
import { getBorderClass } from "./utils/get-border-class"
|
|
9
10
|
|
|
10
11
|
const LazyLoadedMarkdownEditor = lazy(
|
|
11
12
|
() => import("./LazyLoadedMarkdownEditor"),
|
|
@@ -14,11 +15,13 @@ const LazyLoadedMarkdownEditor = lazy(
|
|
|
14
15
|
export default function MarkdownEditor<TFieldValues extends FieldValues>({
|
|
15
16
|
control,
|
|
16
17
|
name,
|
|
18
|
+
required = false,
|
|
17
19
|
label,
|
|
18
20
|
hint,
|
|
19
21
|
}: {
|
|
20
22
|
name: Path<TFieldValues>
|
|
21
23
|
label: string
|
|
24
|
+
required?: boolean
|
|
22
25
|
hint?: string
|
|
23
26
|
control: Control<TFieldValues>
|
|
24
27
|
}) {
|
|
@@ -28,16 +31,21 @@ export default function MarkdownEditor<TFieldValues extends FieldValues>({
|
|
|
28
31
|
return (
|
|
29
32
|
<fieldset key={name} className="group">
|
|
30
33
|
<legend>
|
|
31
|
-
<Label
|
|
34
|
+
<Label
|
|
35
|
+
required={required}
|
|
36
|
+
label={label}
|
|
37
|
+
isDirty={isDirty}
|
|
38
|
+
isInvalid={!!errorMessage}
|
|
39
|
+
/>
|
|
32
40
|
</legend>
|
|
33
41
|
|
|
34
42
|
<div
|
|
35
|
-
className={`overflow-hidden rounded-
|
|
43
|
+
className={`overflow-hidden rounded-xl rounded-ss-none ${getBorderClass({ isDirty, errorMessage, focusWithin: true })} shadow-sm`}
|
|
36
44
|
>
|
|
37
45
|
<Suspense
|
|
38
46
|
fallback={
|
|
39
|
-
<div className="h-[22.75rem] w-full bg-
|
|
40
|
-
<div className="h-10 bg-
|
|
47
|
+
<div className="h-[22.75rem] w-full bg-slate-50">
|
|
48
|
+
<div className="h-10 bg-slate-100"></div>
|
|
41
49
|
</div>
|
|
42
50
|
}
|
|
43
51
|
>
|