@jant/core 0.3.23 → 0.3.25
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/dist/app.js +50 -26
- package/dist/db/schema.js +72 -47
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/constants.js +2 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +30 -6
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +7 -11
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme.js +4 -4
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +95 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +27 -33
- package/dist/routes/api/search.js +4 -5
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -42
- package/dist/routes/dash/index.js +3 -3
- package/dist/routes/dash/media.js +2 -2
- package/dist/routes/dash/pages.js +440 -106
- package/dist/routes/dash/posts.js +27 -37
- package/dist/routes/dash/redirects.js +2 -2
- package/dist/routes/dash/settings.js +79 -5
- package/dist/routes/feed/rss.js +4 -6
- package/dist/routes/feed/sitemap.js +11 -8
- package/dist/routes/pages/archive.js +13 -15
- package/dist/routes/pages/collection.js +12 -9
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +32 -0
- package/dist/routes/pages/home.js +19 -68
- package/dist/routes/pages/page.js +57 -29
- package/dist/routes/pages/post.js +7 -17
- package/dist/routes/pages/search.js +5 -9
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +84 -0
- package/dist/services/post.js +102 -69
- package/dist/services/search.js +24 -18
- package/dist/types.js +24 -40
- package/dist/ui/compose/ComposeDialog.js +452 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
- package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
- package/dist/{theme/components → ui/dash}/PostList.js +18 -13
- package/dist/ui/dash/StatusBadge.js +46 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/feed/LinkCard.js +72 -0
- package/dist/ui/feed/NoteCard.js +58 -0
- package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
- package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +141 -0
- package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
- package/dist/ui/pages/CollectionPage.js +70 -0
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
- package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
- package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
- package/dist/ui/shared/MediaGallery.js +35 -0
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
- package/dist/ui/shared/index.js +5 -0
- package/package.json +2 -9
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +53 -73
- package/src/app.tsx +56 -28
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +14 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +443 -240
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +443 -240
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +443 -240
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +29 -42
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +201 -99
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
- package/src/lib/__tests__/view.test.ts +204 -50
- package/src/lib/constants.ts +2 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +45 -8
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +7 -14
- package/src/lib/schemas.ts +119 -51
- package/src/lib/theme.ts +5 -5
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +141 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +46 -0
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +28 -28
- package/src/routes/api/search.ts +3 -3
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/collections.tsx +20 -42
- package/src/routes/dash/index.tsx +3 -3
- package/src/routes/dash/media.tsx +2 -2
- package/src/routes/dash/pages.tsx +480 -122
- package/src/routes/dash/posts.tsx +42 -54
- package/src/routes/dash/redirects.tsx +2 -2
- package/src/routes/dash/settings.tsx +83 -5
- package/src/routes/feed/rss.ts +4 -3
- package/src/routes/feed/sitemap.ts +15 -5
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +15 -15
- package/src/routes/pages/collection.tsx +16 -9
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +38 -0
- package/src/routes/pages/home.tsx +21 -92
- package/src/routes/pages/page.tsx +62 -27
- package/src/routes/pages/post.tsx +6 -18
- package/src/routes/pages/search.tsx +3 -7
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +432 -197
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +136 -0
- package/src/services/post.ts +141 -101
- package/src/services/search.ts +38 -27
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +491 -0
- package/src/types.ts +212 -198
- package/src/ui/compose/ComposeDialog.tsx +395 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/ui/dash/FormatBadge.tsx +28 -0
- package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
- package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
- package/src/ui/dash/PostList.tsx +101 -0
- package/src/ui/dash/StatusBadge.tsx +61 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/feed/LinkCard.tsx +72 -0
- package/src/ui/feed/NoteCard.tsx +63 -0
- package/src/ui/feed/QuoteCard.tsx +68 -0
- package/src/ui/feed/ThreadPreview.tsx +48 -0
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +150 -0
- package/src/ui/pages/ArchivePage.tsx +162 -0
- package/src/ui/pages/CollectionPage.tsx +70 -0
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/ui/pages/HomePage.tsx +37 -0
- package/src/ui/pages/PostPage.tsx +56 -0
- package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
- package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
- package/src/ui/shared/MediaGallery.tsx +59 -0
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -49
- package/dist/routes/api/timeline.js +0 -120
- package/dist/routes/dash/navigation.js +0 -288
- package/dist/theme/components/MediaGallery.js +0 -107
- package/dist/theme/components/VisibilityBadge.js +0 -37
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/pages/HomePage.js +0 -25
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
- package/src/lib/__tests__/theme-components.test.ts +0 -126
- package/src/lib/theme-components.ts +0 -68
- package/src/routes/api/timeline.tsx +0 -159
- package/src/routes/dash/navigation.tsx +0 -316
- package/src/theme/components/MediaGallery.tsx +0 -128
- package/src/theme/components/PostList.tsx +0 -92
- package/src/theme/components/TypeBadge.tsx +0 -37
- package/src/theme/components/VisibilityBadge.tsx +0 -45
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/index.ts +0 -83
- package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/pages/HomePage.tsx +0 -41
- package/src/themes/minimal/pages/PostPage.tsx +0 -43
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
- package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
- /package/dist/{theme → ui}/color-themes.js +0 -0
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme → ui}/color-themes.ts +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createNavItemService } from "../navigation.js";
|
|
4
|
+
import { createPageService } from "../page.js";
|
|
4
5
|
import type { Database } from "../../db/index.js";
|
|
5
6
|
|
|
6
|
-
describe("
|
|
7
|
+
describe("NavItemService", () => {
|
|
7
8
|
let db: Database;
|
|
8
|
-
let
|
|
9
|
+
let navItemService: ReturnType<typeof createNavItemService>;
|
|
10
|
+
let pageService: ReturnType<typeof createPageService>;
|
|
9
11
|
|
|
10
12
|
beforeEach(() => {
|
|
11
13
|
const testDb = createTestDatabase();
|
|
12
14
|
db = testDb.db as unknown as Database;
|
|
13
|
-
|
|
15
|
+
navItemService = createNavItemService(db);
|
|
16
|
+
pageService = createPageService(db);
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
describe("create", () => {
|
|
17
|
-
it("creates a
|
|
18
|
-
const
|
|
20
|
+
it("creates a nav item with auto-assigned position", async () => {
|
|
21
|
+
const item = await navItemService.create({
|
|
22
|
+
type: "link",
|
|
19
23
|
label: "Home",
|
|
20
24
|
url: "/",
|
|
21
25
|
});
|
|
22
26
|
|
|
23
|
-
expect(
|
|
24
|
-
expect(
|
|
25
|
-
expect(
|
|
26
|
-
expect(
|
|
27
|
+
expect(item.type).toBe("link");
|
|
28
|
+
expect(item.label).toBe("Home");
|
|
29
|
+
expect(item.url).toBe("/");
|
|
30
|
+
expect(item.pageId).toBeNull();
|
|
31
|
+
expect(item.position).toBe(0);
|
|
32
|
+
expect(item.id).toBe(1);
|
|
27
33
|
});
|
|
28
34
|
|
|
29
|
-
it("
|
|
30
|
-
await
|
|
31
|
-
|
|
35
|
+
it("creates a page-type nav item with pageId", async () => {
|
|
36
|
+
const page = await pageService.create({ slug: "about", title: "About" });
|
|
37
|
+
|
|
38
|
+
const item = await navItemService.create({
|
|
39
|
+
type: "page",
|
|
40
|
+
label: "About",
|
|
41
|
+
url: "/about",
|
|
42
|
+
pageId: page.id,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(item.type).toBe("page");
|
|
46
|
+
expect(item.label).toBe("About");
|
|
47
|
+
expect(item.url).toBe("/about");
|
|
48
|
+
expect(item.pageId).toBe(page.id);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("auto-increments position for subsequent items", async () => {
|
|
52
|
+
await navItemService.create({
|
|
53
|
+
type: "link",
|
|
54
|
+
label: "Home",
|
|
55
|
+
url: "/",
|
|
56
|
+
});
|
|
57
|
+
const second = await navItemService.create({
|
|
58
|
+
type: "link",
|
|
32
59
|
label: "Archive",
|
|
33
60
|
url: "/archive",
|
|
34
61
|
});
|
|
@@ -37,87 +64,130 @@ describe("NavigationLinkService", () => {
|
|
|
37
64
|
});
|
|
38
65
|
|
|
39
66
|
it("uses provided position when specified", async () => {
|
|
40
|
-
const
|
|
67
|
+
const item = await navItemService.create({
|
|
68
|
+
type: "link",
|
|
41
69
|
label: "Home",
|
|
42
70
|
url: "/",
|
|
43
71
|
position: 5,
|
|
44
72
|
});
|
|
45
73
|
|
|
46
|
-
expect(
|
|
74
|
+
expect(item.position).toBe(5);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("sets createdAt and updatedAt timestamps", async () => {
|
|
78
|
+
const item = await navItemService.create({
|
|
79
|
+
type: "link",
|
|
80
|
+
label: "Home",
|
|
81
|
+
url: "/",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(item.createdAt).toBeGreaterThan(0);
|
|
85
|
+
expect(item.updatedAt).toBeGreaterThan(0);
|
|
86
|
+
expect(item.createdAt).toBe(item.updatedAt);
|
|
47
87
|
});
|
|
48
88
|
});
|
|
49
89
|
|
|
50
90
|
describe("getById", () => {
|
|
51
|
-
it("returns a
|
|
52
|
-
const created = await
|
|
91
|
+
it("returns a nav item by ID", async () => {
|
|
92
|
+
const created = await navItemService.create({
|
|
93
|
+
type: "link",
|
|
53
94
|
label: "Home",
|
|
54
95
|
url: "/",
|
|
55
96
|
});
|
|
56
97
|
|
|
57
|
-
const found = await
|
|
98
|
+
const found = await navItemService.getById(created.id);
|
|
58
99
|
expect(found).not.toBeNull();
|
|
59
100
|
expect(found?.label).toBe("Home");
|
|
101
|
+
expect(found?.type).toBe("link");
|
|
60
102
|
});
|
|
61
103
|
|
|
62
104
|
it("returns null for non-existent ID", async () => {
|
|
63
|
-
const found = await
|
|
105
|
+
const found = await navItemService.getById(9999);
|
|
64
106
|
expect(found).toBeNull();
|
|
65
107
|
});
|
|
66
108
|
});
|
|
67
109
|
|
|
68
110
|
describe("list", () => {
|
|
69
|
-
it("returns empty array when no
|
|
70
|
-
const
|
|
71
|
-
expect(
|
|
111
|
+
it("returns empty array when no items exist", async () => {
|
|
112
|
+
const items = await navItemService.list();
|
|
113
|
+
expect(items).toEqual([]);
|
|
72
114
|
});
|
|
73
115
|
|
|
74
|
-
it("returns
|
|
75
|
-
await
|
|
116
|
+
it("returns items ordered by position", async () => {
|
|
117
|
+
await navItemService.create({
|
|
118
|
+
type: "link",
|
|
76
119
|
label: "C",
|
|
77
120
|
url: "/c",
|
|
78
121
|
position: 2,
|
|
79
122
|
});
|
|
80
|
-
await
|
|
123
|
+
await navItemService.create({
|
|
124
|
+
type: "link",
|
|
81
125
|
label: "A",
|
|
82
126
|
url: "/a",
|
|
83
127
|
position: 0,
|
|
84
128
|
});
|
|
85
|
-
await
|
|
129
|
+
await navItemService.create({
|
|
130
|
+
type: "page",
|
|
86
131
|
label: "B",
|
|
87
132
|
url: "/b",
|
|
88
133
|
position: 1,
|
|
89
134
|
});
|
|
90
135
|
|
|
91
|
-
const
|
|
92
|
-
expect(
|
|
93
|
-
expect(
|
|
94
|
-
expect(
|
|
95
|
-
expect(
|
|
136
|
+
const items = await navItemService.list();
|
|
137
|
+
expect(items).toHaveLength(3);
|
|
138
|
+
expect(items[0]?.label).toBe("A");
|
|
139
|
+
expect(items[1]?.label).toBe("B");
|
|
140
|
+
expect(items[2]?.label).toBe("C");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns items with correct types", async () => {
|
|
144
|
+
const page = await pageService.create({ slug: "about", title: "About" });
|
|
145
|
+
|
|
146
|
+
await navItemService.create({
|
|
147
|
+
type: "link",
|
|
148
|
+
label: "External",
|
|
149
|
+
url: "https://example.com",
|
|
150
|
+
});
|
|
151
|
+
await navItemService.create({
|
|
152
|
+
type: "page",
|
|
153
|
+
label: "About",
|
|
154
|
+
url: "/about",
|
|
155
|
+
pageId: page.id,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const items = await navItemService.list();
|
|
159
|
+
expect(items).toHaveLength(2);
|
|
160
|
+
expect(items[0]?.type).toBe("link");
|
|
161
|
+
expect(items[1]?.type).toBe("page");
|
|
162
|
+
expect(items[1]?.pageId).toBe(page.id);
|
|
96
163
|
});
|
|
97
164
|
});
|
|
98
165
|
|
|
99
166
|
describe("update", () => {
|
|
100
|
-
it("updates a
|
|
101
|
-
const created = await
|
|
167
|
+
it("updates a nav item's label", async () => {
|
|
168
|
+
const created = await navItemService.create({
|
|
169
|
+
type: "link",
|
|
102
170
|
label: "Home",
|
|
103
171
|
url: "/",
|
|
104
172
|
});
|
|
105
173
|
|
|
106
|
-
const updated = await
|
|
174
|
+
const updated = await navItemService.update(created.id, {
|
|
107
175
|
label: "Main Page",
|
|
108
176
|
});
|
|
109
177
|
|
|
110
178
|
expect(updated?.label).toBe("Main Page");
|
|
111
179
|
expect(updated?.url).toBe("/");
|
|
180
|
+
expect(updated?.type).toBe("link");
|
|
112
181
|
});
|
|
113
182
|
|
|
114
|
-
it("updates a
|
|
115
|
-
const created = await
|
|
183
|
+
it("updates a nav item's url", async () => {
|
|
184
|
+
const created = await navItemService.create({
|
|
185
|
+
type: "link",
|
|
116
186
|
label: "Blog",
|
|
117
187
|
url: "/blog",
|
|
118
188
|
});
|
|
119
189
|
|
|
120
|
-
const updated = await
|
|
190
|
+
const updated = await navItemService.update(created.id, {
|
|
121
191
|
url: "/posts",
|
|
122
192
|
});
|
|
123
193
|
|
|
@@ -125,89 +195,93 @@ describe("NavigationLinkService", () => {
|
|
|
125
195
|
expect(updated?.label).toBe("Blog");
|
|
126
196
|
});
|
|
127
197
|
|
|
198
|
+
it("updates a nav item's type", async () => {
|
|
199
|
+
const page = await pageService.create({ slug: "about", title: "About" });
|
|
200
|
+
|
|
201
|
+
const created = await navItemService.create({
|
|
202
|
+
type: "link",
|
|
203
|
+
label: "About",
|
|
204
|
+
url: "/about",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const updated = await navItemService.update(created.id, {
|
|
208
|
+
type: "page",
|
|
209
|
+
pageId: page.id,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(updated?.type).toBe("page");
|
|
213
|
+
expect(updated?.pageId).toBe(page.id);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("updates updatedAt timestamp", async () => {
|
|
217
|
+
const created = await navItemService.create({
|
|
218
|
+
type: "link",
|
|
219
|
+
label: "Home",
|
|
220
|
+
url: "/",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const updated = await navItemService.update(created.id, {
|
|
224
|
+
label: "Updated",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(updated?.updatedAt).toBeGreaterThanOrEqual(created.updatedAt);
|
|
228
|
+
});
|
|
229
|
+
|
|
128
230
|
it("returns null for non-existent ID", async () => {
|
|
129
|
-
const result = await
|
|
231
|
+
const result = await navItemService.update(9999, { label: "Nope" });
|
|
130
232
|
expect(result).toBeNull();
|
|
131
233
|
});
|
|
132
234
|
});
|
|
133
235
|
|
|
134
236
|
describe("delete", () => {
|
|
135
|
-
it("deletes a
|
|
136
|
-
const
|
|
237
|
+
it("deletes a nav item by ID", async () => {
|
|
238
|
+
const item = await navItemService.create({
|
|
239
|
+
type: "link",
|
|
137
240
|
label: "Home",
|
|
138
241
|
url: "/",
|
|
139
242
|
});
|
|
140
|
-
const result = await
|
|
243
|
+
const result = await navItemService.delete(item.id);
|
|
141
244
|
|
|
142
245
|
expect(result).toBe(true);
|
|
143
246
|
|
|
144
|
-
const found = await
|
|
247
|
+
const found = await navItemService.getById(item.id);
|
|
145
248
|
expect(found).toBeNull();
|
|
146
249
|
});
|
|
147
250
|
|
|
148
251
|
it("returns false for non-existent ID", async () => {
|
|
149
|
-
const result = await
|
|
252
|
+
const result = await navItemService.delete(9999);
|
|
150
253
|
expect(result).toBe(false);
|
|
151
254
|
});
|
|
152
255
|
});
|
|
153
256
|
|
|
154
257
|
describe("reorder", () => {
|
|
155
258
|
it("updates positions to match array order", async () => {
|
|
156
|
-
const a = await
|
|
259
|
+
const a = await navItemService.create({
|
|
260
|
+
type: "link",
|
|
157
261
|
label: "A",
|
|
158
262
|
url: "/a",
|
|
159
263
|
});
|
|
160
|
-
const b = await
|
|
264
|
+
const b = await navItemService.create({
|
|
265
|
+
type: "link",
|
|
161
266
|
label: "B",
|
|
162
267
|
url: "/b",
|
|
163
268
|
});
|
|
164
|
-
const c = await
|
|
269
|
+
const c = await navItemService.create({
|
|
270
|
+
type: "link",
|
|
165
271
|
label: "C",
|
|
166
272
|
url: "/c",
|
|
167
273
|
});
|
|
168
274
|
|
|
169
275
|
// Reverse the order
|
|
170
|
-
await
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
expect(
|
|
174
|
-
expect(
|
|
175
|
-
expect(
|
|
176
|
-
expect(
|
|
177
|
-
expect(
|
|
178
|
-
expect(
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("ensureDefaults", () => {
|
|
183
|
-
it("creates default links when table is empty", async () => {
|
|
184
|
-
const links = await navigationService.ensureDefaults();
|
|
185
|
-
|
|
186
|
-
expect(links).toHaveLength(3);
|
|
187
|
-
expect(links[0]?.label).toBe("Home");
|
|
188
|
-
expect(links[0]?.url).toBe("/");
|
|
189
|
-
expect(links[1]?.label).toBe("Archive");
|
|
190
|
-
expect(links[1]?.url).toBe("/archive");
|
|
191
|
-
expect(links[2]?.label).toBe("RSS");
|
|
192
|
-
expect(links[2]?.url).toBe("/feed");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("returns existing links without creating new ones", async () => {
|
|
196
|
-
await navigationService.create({ label: "Custom", url: "/custom" });
|
|
197
|
-
|
|
198
|
-
const links = await navigationService.ensureDefaults();
|
|
199
|
-
|
|
200
|
-
expect(links).toHaveLength(1);
|
|
201
|
-
expect(links[0]?.label).toBe("Custom");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("is idempotent - calling twice returns same result", async () => {
|
|
205
|
-
const first = await navigationService.ensureDefaults();
|
|
206
|
-
const second = await navigationService.ensureDefaults();
|
|
207
|
-
|
|
208
|
-
expect(first).toHaveLength(3);
|
|
209
|
-
expect(second).toHaveLength(3);
|
|
210
|
-
expect(first[0]?.id).toBe(second[0]?.id);
|
|
276
|
+
await navItemService.reorder([c.id, b.id, a.id]);
|
|
277
|
+
|
|
278
|
+
const items = await navItemService.list();
|
|
279
|
+
expect(items[0]?.label).toBe("C");
|
|
280
|
+
expect(items[0]?.position).toBe(0);
|
|
281
|
+
expect(items[1]?.label).toBe("B");
|
|
282
|
+
expect(items[1]?.position).toBe(1);
|
|
283
|
+
expect(items[2]?.label).toBe("A");
|
|
284
|
+
expect(items[2]?.position).toBe(2);
|
|
211
285
|
});
|
|
212
286
|
});
|
|
213
287
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
+
import { createPageService } from "../page.js";
|
|
4
|
+
import { createNavItemService } from "../navigation.js";
|
|
5
|
+
import type { Database } from "../../db/index.js";
|
|
6
|
+
|
|
7
|
+
describe("PageService", () => {
|
|
8
|
+
let db: Database;
|
|
9
|
+
let pageService: ReturnType<typeof createPageService>;
|
|
10
|
+
let navItemService: ReturnType<typeof createNavItemService>;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
const testDb = createTestDatabase();
|
|
14
|
+
db = testDb.db as unknown as Database;
|
|
15
|
+
pageService = createPageService(db);
|
|
16
|
+
navItemService = createNavItemService(db);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("listNotInNav", () => {
|
|
20
|
+
it("returns all pages when none are in navigation", async () => {
|
|
21
|
+
await pageService.create({ slug: "about", title: "About" });
|
|
22
|
+
await pageService.create({ slug: "contact", title: "Contact" });
|
|
23
|
+
|
|
24
|
+
const pages = await pageService.listNotInNav();
|
|
25
|
+
expect(pages).toHaveLength(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("excludes pages that have a nav item", async () => {
|
|
29
|
+
const aboutPage = await pageService.create({
|
|
30
|
+
slug: "about",
|
|
31
|
+
title: "About",
|
|
32
|
+
});
|
|
33
|
+
await pageService.create({ slug: "contact", title: "Contact" });
|
|
34
|
+
|
|
35
|
+
// Add "About" to navigation
|
|
36
|
+
await navItemService.create({
|
|
37
|
+
type: "page",
|
|
38
|
+
label: "About",
|
|
39
|
+
url: "/about",
|
|
40
|
+
pageId: aboutPage.id,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const pages = await pageService.listNotInNav();
|
|
44
|
+
expect(pages).toHaveLength(1);
|
|
45
|
+
expect(pages[0]?.slug).toBe("contact");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns empty array when all pages are in navigation", async () => {
|
|
49
|
+
const aboutPage = await pageService.create({
|
|
50
|
+
slug: "about",
|
|
51
|
+
title: "About",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await navItemService.create({
|
|
55
|
+
type: "page",
|
|
56
|
+
label: "About",
|
|
57
|
+
url: "/about",
|
|
58
|
+
pageId: aboutPage.id,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const pages = await pageService.listNotInNav();
|
|
62
|
+
expect(pages).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns empty array when no pages exist", async () => {
|
|
66
|
+
const pages = await pageService.listNotInNav();
|
|
67
|
+
expect(pages).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("is not affected by link-type nav items (no pageId)", async () => {
|
|
71
|
+
await pageService.create({ slug: "about", title: "About" });
|
|
72
|
+
|
|
73
|
+
// Link-type nav items have no pageId
|
|
74
|
+
await navItemService.create({
|
|
75
|
+
type: "link",
|
|
76
|
+
label: "External",
|
|
77
|
+
url: "https://example.com",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const pages = await pageService.listNotInNav();
|
|
81
|
+
expect(pages).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns multiple pages correctly", async () => {
|
|
85
|
+
await pageService.create({ slug: "first", title: "First" });
|
|
86
|
+
await pageService.create({ slug: "second", title: "Second" });
|
|
87
|
+
await pageService.create({ slug: "third", title: "Third" });
|
|
88
|
+
|
|
89
|
+
// Add one to nav
|
|
90
|
+
const pages = await pageService.list();
|
|
91
|
+
await navItemService.create({
|
|
92
|
+
type: "page",
|
|
93
|
+
label: "Second",
|
|
94
|
+
url: "/second",
|
|
95
|
+
pageId: pages.find((p) => p.slug === "second")!.id,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const notInNav = await pageService.listNotInNav();
|
|
99
|
+
expect(notInNav).toHaveLength(2);
|
|
100
|
+
const slugs = notInNav.map((p) => p.slug);
|
|
101
|
+
expect(slugs).toContain("first");
|
|
102
|
+
expect(slugs).toContain("third");
|
|
103
|
+
expect(slugs).not.toContain("second");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|