@jant/core 0.3.35 → 0.3.36
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/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-8Dj-5CLW.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +3109 -2294
- package/dist/index.js +3026 -2778
- package/package.json +13 -4
- package/src/__tests__/helpers/app.ts +1 -1
- package/src/__tests__/helpers/db.ts +6 -0
- package/src/app.tsx +1 -5
- package/src/{lib → client}/avatar-upload.ts +1 -1
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/{ui → client}/components/__tests__/jant-compose-dialog.test.ts +46 -14
- package/src/{ui → client}/components/__tests__/jant-compose-editor.test.ts +64 -24
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +24 -14
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +45 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/{ui → client}/components/compose-types.ts +3 -1
- package/src/{ui → client}/components/jant-collection-form.ts +301 -182
- package/src/client/components/jant-collection-sidebar.ts +801 -0
- package/src/{ui → client}/components/jant-compose-dialog.ts +231 -1
- package/src/client/components/jant-compose-editor.ts +1249 -0
- package/src/client/components/jant-compose-fullscreen.ts +338 -0
- package/src/client/components/jant-media-lightbox.ts +257 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +143 -84
- package/src/{ui → client}/components/jant-post-form.ts +57 -8
- package/src/{ui → client}/components/jant-settings-general.ts +2 -2
- package/src/{ui → client}/components/nav-manager-types.ts +3 -0
- package/src/{ui → client}/components/post-form-template.ts +35 -31
- package/src/{ui → client}/components/post-form-types.ts +7 -3
- package/src/{lib → client}/compose-bridge.ts +9 -7
- package/src/client/lazy-slugify.ts +51 -0
- package/src/{lib → client}/media-upload.ts +16 -3
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/client/page-slug-bridge.ts +42 -0
- package/src/{lib → client}/post-form-bridge.ts +2 -2
- package/src/{lib → client}/settings-bridge.ts +3 -3
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +40 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +60 -0
- package/src/client/tiptap/image-node.ts +488 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +140 -0
- package/src/client/tiptap/slash-commands.ts +328 -0
- package/src/{types → client/types}/sortablejs.d.ts +1 -1
- package/src/client.ts +24 -17
- package/src/db/migrations/0012_add_tiptap_columns.sql +2 -0
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +8 -0
- package/src/db/schema.ts +6 -1
- package/src/i18n/locales/en.po +641 -215
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +642 -204
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +642 -204
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +9 -6
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +9 -9
- package/src/lib/emoji-catalog.ts +146 -0
- package/src/lib/feed.ts +1 -1
- package/src/lib/media-helpers.ts +10 -9
- package/src/lib/render.tsx +4 -3
- package/src/lib/resolve-config.ts +8 -1
- package/src/lib/schemas.ts +2 -3
- package/src/lib/summary.ts +92 -0
- package/src/lib/timeline.ts +2 -0
- package/src/lib/tiptap-render.ts +196 -0
- package/src/lib/upload.ts +97 -9
- package/src/lib/url.ts +7 -23
- package/src/lib/view.ts +33 -19
- package/src/middleware/error-handler.ts +3 -3
- package/src/preset.css +38 -0
- package/src/routes/api/collections.ts +20 -3
- package/src/routes/api/posts.ts +48 -33
- package/src/routes/api/upload.ts +7 -5
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +26 -11
- package/src/routes/auth/signin.tsx +10 -7
- package/src/routes/compose.tsx +20 -11
- package/src/routes/dash/__tests__/settings-avatar.test.ts +43 -8
- package/src/routes/dash/index.tsx +7 -1
- package/src/routes/dash/media.tsx +3 -0
- package/src/routes/dash/pages.tsx +8 -2
- package/src/routes/dash/posts.tsx +6 -2
- package/src/routes/dash/redirects.tsx +15 -9
- package/src/routes/dash/settings.tsx +336 -32
- package/src/routes/feed/__tests__/rss.test.ts +7 -7
- package/src/routes/feed/rss.ts +8 -6
- package/src/routes/pages/__tests__/featured.test.ts +6 -7
- package/src/routes/pages/archive.tsx +11 -7
- package/src/routes/pages/collection.tsx +32 -15
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +1 -1
- package/src/routes/pages/home.tsx +1 -1
- package/src/services/__tests__/post.test.ts +124 -33
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/page.ts +16 -3
- package/src/services/post.ts +96 -37
- package/src/services/search.ts +4 -2
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +240 -60
- package/src/styles/tokens.css +10 -0
- package/src/styles/ui.css +1157 -81
- package/src/types/bindings.ts +5 -0
- package/src/types/config.ts +23 -1
- package/src/types/constants.ts +3 -0
- package/src/types/entities.ts +9 -2
- package/src/types/operations.ts +9 -3
- package/src/types/props.ts +3 -3
- package/src/types/views.ts +3 -2
- package/src/ui/compose/ComposeDialog.tsx +24 -7
- package/src/ui/dash/PageForm.tsx +2 -0
- package/src/ui/dash/PostList.tsx +5 -5
- package/src/ui/dash/StatusBadge.tsx +13 -5
- package/src/ui/dash/appearance/AdvancedContent.tsx +52 -61
- package/src/ui/dash/appearance/ColorThemeContent.tsx +30 -35
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +107 -96
- package/src/ui/dash/media/MediaListContent.tsx +9 -4
- package/src/ui/dash/media/ViewMediaContent.tsx +2 -2
- package/src/ui/dash/pages/PagesContent.tsx +2 -1
- package/src/ui/dash/posts/PostForm.tsx +19 -7
- package/src/ui/dash/settings/AccountContent.tsx +133 -138
- package/src/ui/dash/settings/AvatarContent.tsx +70 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SettingsRootContent.tsx +236 -0
- package/src/ui/layouts/DashLayout.tsx +157 -75
- package/src/ui/layouts/SiteLayout.tsx +13 -13
- package/src/ui/pages/ArchivePage.tsx +10 -7
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/SearchPage.tsx +1 -1
- package/src/ui/shared/CollectionsSidebar.tsx +228 -3
- package/src/ui/shared/MediaGallery.tsx +179 -41
- package/src/lib/collections-reorder.ts +0 -28
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- /package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +0 -0
- /package/src/{ui → client}/components/jant-settings-avatar.ts +0 -0
- /package/src/{ui → client}/components/settings-types.ts +0 -0
- /package/src/{lib → client}/image-processor.ts +0 -0
- /package/src/{lib → client}/toast.ts +0 -0
|
@@ -26,24 +26,23 @@ describe("Featured Page - Data Logic", () => {
|
|
|
26
26
|
await postService.create({
|
|
27
27
|
format: "note",
|
|
28
28
|
body: "Featured post",
|
|
29
|
-
|
|
29
|
+
visibility: "featured",
|
|
30
30
|
status: "published",
|
|
31
31
|
});
|
|
32
32
|
await postService.create({
|
|
33
33
|
format: "note",
|
|
34
34
|
body: "Normal post",
|
|
35
|
-
featured: false,
|
|
36
35
|
status: "published",
|
|
37
36
|
});
|
|
38
37
|
await postService.create({
|
|
39
38
|
format: "note",
|
|
40
39
|
body: "Draft featured",
|
|
41
|
-
|
|
40
|
+
visibility: "featured",
|
|
42
41
|
status: "draft",
|
|
43
42
|
});
|
|
44
43
|
|
|
45
44
|
const posts = await postService.list({
|
|
46
|
-
|
|
45
|
+
visibility: "featured",
|
|
47
46
|
status: "published",
|
|
48
47
|
excludeReplies: true,
|
|
49
48
|
});
|
|
@@ -60,7 +59,7 @@ describe("Featured Page - Data Logic", () => {
|
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
const posts = await postService.list({
|
|
63
|
-
|
|
62
|
+
visibility: "featured",
|
|
64
63
|
status: "published",
|
|
65
64
|
excludeReplies: true,
|
|
66
65
|
});
|
|
@@ -72,7 +71,7 @@ describe("Featured Page - Data Logic", () => {
|
|
|
72
71
|
const root = await postService.create({
|
|
73
72
|
format: "note",
|
|
74
73
|
body: "Featured root",
|
|
75
|
-
|
|
74
|
+
visibility: "featured",
|
|
76
75
|
status: "published",
|
|
77
76
|
});
|
|
78
77
|
|
|
@@ -84,7 +83,7 @@ describe("Featured Page - Data Logic", () => {
|
|
|
84
83
|
});
|
|
85
84
|
|
|
86
85
|
const posts = await postService.list({
|
|
87
|
-
|
|
86
|
+
visibility: "featured",
|
|
88
87
|
status: "published",
|
|
89
88
|
excludeReplies: true,
|
|
90
89
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Archive Page Route
|
|
3
3
|
*
|
|
4
|
-
* Shows all posts, optionally filtered by format or
|
|
4
|
+
* Shows all posts, optionally filtered by format or visibility
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings, Format } from "../../types.js";
|
|
8
|
+
import type { Bindings, Format, Visibility } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../types/app-context.js";
|
|
10
|
-
import { FORMATS } from "../../types.js";
|
|
10
|
+
import { FORMATS, VISIBILITIES } from "../../types.js";
|
|
11
11
|
import { ArchivePage } from "../../ui/pages/ArchivePage.js";
|
|
12
12
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
13
13
|
import { renderPublicPage } from "../../lib/render.js";
|
|
@@ -24,8 +24,12 @@ archiveRoutes.get("/", async (c) => {
|
|
|
24
24
|
const formatParam = c.req.query("format") as Format | undefined;
|
|
25
25
|
const format =
|
|
26
26
|
formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
|
|
27
|
-
const
|
|
28
|
-
const
|
|
27
|
+
const visibilityParam = c.req.query("visibility") as Visibility | undefined;
|
|
28
|
+
const visibility =
|
|
29
|
+
visibilityParam &&
|
|
30
|
+
(VISIBILITIES as readonly string[]).includes(visibilityParam)
|
|
31
|
+
? visibilityParam
|
|
32
|
+
: undefined;
|
|
29
33
|
|
|
30
34
|
// Parse cursor
|
|
31
35
|
const cursorParam = c.req.query("cursor");
|
|
@@ -37,7 +41,7 @@ archiveRoutes.get("/", async (c) => {
|
|
|
37
41
|
const posts = await c.var.services.posts.list({
|
|
38
42
|
format,
|
|
39
43
|
status: "published",
|
|
40
|
-
|
|
44
|
+
visibility,
|
|
41
45
|
excludeReplies: true,
|
|
42
46
|
cursor,
|
|
43
47
|
limit: PAGE_SIZE + 1,
|
|
@@ -78,7 +82,7 @@ archiveRoutes.get("/", async (c) => {
|
|
|
78
82
|
hasMore={hasMore}
|
|
79
83
|
nextCursor={nextCursor}
|
|
80
84
|
format={format}
|
|
81
|
-
|
|
85
|
+
visibility={visibility}
|
|
82
86
|
/>
|
|
83
87
|
),
|
|
84
88
|
});
|
|
@@ -8,11 +8,7 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
8
8
|
import { CollectionPage } from "../../ui/pages/CollectionPage.js";
|
|
9
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
|
-
import {
|
|
12
|
-
createMediaContext,
|
|
13
|
-
toPostViewsFromPosts,
|
|
14
|
-
toPostViews,
|
|
15
|
-
} from "../../lib/view.js";
|
|
11
|
+
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
16
12
|
import { defaultRssRenderer } from "../../lib/feed.js";
|
|
17
13
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
18
14
|
import { CollectionsSidebar } from "../../ui/shared/CollectionsSidebar.js";
|
|
@@ -27,35 +23,56 @@ collectionRoutes.get("/:slug", async (c) => {
|
|
|
27
23
|
const collection = await c.var.services.collections.getBySlug(slug);
|
|
28
24
|
if (!collection) return c.notFound();
|
|
29
25
|
|
|
30
|
-
// Fetch posts
|
|
31
|
-
const [posts, allCollections] = await Promise.all([
|
|
26
|
+
// Fetch posts, all collections, dividers, and post counts in parallel
|
|
27
|
+
const [posts, allCollections, dividers, postCounts] = await Promise.all([
|
|
32
28
|
c.var.services.posts.list({
|
|
33
29
|
collectionId: collection.id,
|
|
34
30
|
status: "published",
|
|
35
31
|
excludeReplies: true,
|
|
36
32
|
}),
|
|
37
33
|
c.var.services.collections.list(),
|
|
34
|
+
c.var.services.collections.listDividers(),
|
|
35
|
+
c.var.services.collections.getPostCounts(),
|
|
38
36
|
]);
|
|
39
37
|
|
|
40
38
|
const navData = await getNavigationData(c);
|
|
41
39
|
|
|
42
|
-
//
|
|
40
|
+
// Batch-load media for posts
|
|
41
|
+
const postIds = posts.map((p) => p.id);
|
|
42
|
+
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
43
43
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
44
|
-
const
|
|
44
|
+
const mediaMap = buildMediaMap(
|
|
45
|
+
rawMediaMap,
|
|
46
|
+
mediaCtx.r2PublicUrl,
|
|
47
|
+
mediaCtx.imageTransformUrl,
|
|
48
|
+
mediaCtx.s3PublicUrl,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const postViews = toPostViews(
|
|
52
|
+
posts.map((p) => ({
|
|
53
|
+
...p,
|
|
54
|
+
mediaAttachments: mediaMap.get(p.id) ?? [],
|
|
55
|
+
})),
|
|
56
|
+
mediaCtx,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const items = postViews.map((post) => ({ post }));
|
|
45
60
|
|
|
46
61
|
return renderPublicPage(c, {
|
|
47
62
|
title: `${collection.title} - ${navData.siteName}`,
|
|
48
63
|
description: collection.description ?? undefined,
|
|
49
64
|
navData,
|
|
50
65
|
sidebar: (
|
|
51
|
-
<CollectionsSidebar
|
|
66
|
+
<CollectionsSidebar
|
|
67
|
+
collections={allCollections}
|
|
68
|
+
dividers={dividers}
|
|
69
|
+
activeSlug={slug}
|
|
70
|
+
isAuthenticated={navData.isAuthenticated}
|
|
71
|
+
postCounts={postCounts}
|
|
72
|
+
/>
|
|
52
73
|
),
|
|
53
74
|
content: (
|
|
54
|
-
<CollectionPage
|
|
55
|
-
collection={collection}
|
|
56
|
-
posts={postViews}
|
|
57
|
-
hasMore={false}
|
|
58
|
-
/>
|
|
75
|
+
<CollectionPage collection={collection} items={items} hasMore={false} />
|
|
59
76
|
),
|
|
60
77
|
});
|
|
61
78
|
});
|
|
@@ -17,8 +17,9 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
17
17
|
export const collectionsPageRoutes = new Hono<Env>();
|
|
18
18
|
|
|
19
19
|
collectionsPageRoutes.get("/", async (c) => {
|
|
20
|
-
const [allCollections, postCounts] = await Promise.all([
|
|
20
|
+
const [allCollections, dividers, postCounts] = await Promise.all([
|
|
21
21
|
c.var.services.collections.list(),
|
|
22
|
+
c.var.services.collections.listDividers(),
|
|
22
23
|
c.var.services.collections.getPostCounts(),
|
|
23
24
|
]);
|
|
24
25
|
|
|
@@ -32,7 +33,15 @@ collectionsPageRoutes.get("/", async (c) => {
|
|
|
32
33
|
return renderPublicPage(c, {
|
|
33
34
|
title: `Collections - ${navData.siteName}`,
|
|
34
35
|
navData,
|
|
35
|
-
sidebar:
|
|
36
|
+
sidebar: (
|
|
37
|
+
<CollectionsSidebar
|
|
38
|
+
collections={allCollections}
|
|
39
|
+
dividers={dividers}
|
|
40
|
+
activeSlug={undefined}
|
|
41
|
+
isAuthenticated={navData.isAuthenticated}
|
|
42
|
+
postCounts={postCounts}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
36
45
|
content: <CollectionsPage collections={collections} />,
|
|
37
46
|
});
|
|
38
47
|
});
|
|
@@ -28,7 +28,7 @@ homeRoutes.get("/", async (c) => {
|
|
|
28
28
|
if (navData.homeDefaultView === "featured") {
|
|
29
29
|
// Show featured posts on homepage
|
|
30
30
|
const posts = await c.var.services.posts.list({
|
|
31
|
-
|
|
31
|
+
visibility: "featured",
|
|
32
32
|
status: "published",
|
|
33
33
|
excludeReplies: true,
|
|
34
34
|
});
|
|
@@ -22,28 +22,51 @@ describe("PostService", () => {
|
|
|
22
22
|
|
|
23
23
|
describe("create", () => {
|
|
24
24
|
it("creates a note post with required fields", async () => {
|
|
25
|
+
const body = JSON.stringify({
|
|
26
|
+
type: "doc",
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "paragraph",
|
|
30
|
+
content: [{ type: "text", text: "Hello world" }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
25
34
|
const post = await postService.create({
|
|
26
35
|
format: "note",
|
|
27
|
-
body
|
|
36
|
+
body,
|
|
28
37
|
});
|
|
29
38
|
|
|
30
39
|
expect(post.id).toBe(1);
|
|
31
40
|
expect(post.format).toBe("note");
|
|
32
|
-
expect(post.body).toBe(
|
|
41
|
+
expect(post.body).toBe(body);
|
|
33
42
|
expect(post.status).toBe("published"); // default
|
|
34
|
-
expect(post.
|
|
43
|
+
expect(post.visibility).toBe("listed");
|
|
35
44
|
expect(post.pinned).toBe(0);
|
|
36
45
|
expect(post.bodyHtml).toContain("<p>Hello world</p>");
|
|
37
46
|
expect(post.deletedAt).toBeNull();
|
|
38
47
|
});
|
|
39
48
|
|
|
40
49
|
it("creates a post with all fields", async () => {
|
|
50
|
+
const body = JSON.stringify({
|
|
51
|
+
type: "doc",
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "heading",
|
|
55
|
+
attrs: { level: 1 },
|
|
56
|
+
content: [{ type: "text", text: "Introduction" }],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "paragraph",
|
|
60
|
+
content: [{ type: "text", text: "Some content." }],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
41
64
|
const post = await postService.create({
|
|
42
65
|
format: "link",
|
|
43
66
|
title: "My Link",
|
|
44
|
-
body
|
|
67
|
+
body,
|
|
45
68
|
status: "published",
|
|
46
|
-
|
|
69
|
+
visibility: "featured",
|
|
47
70
|
pinned: true,
|
|
48
71
|
path: "my-link",
|
|
49
72
|
url: "https://example.com/source",
|
|
@@ -54,7 +77,7 @@ describe("PostService", () => {
|
|
|
54
77
|
expect(post.format).toBe("link");
|
|
55
78
|
expect(post.title).toBe("My Link");
|
|
56
79
|
expect(post.status).toBe("published");
|
|
57
|
-
expect(post.
|
|
80
|
+
expect(post.visibility).toBe("featured");
|
|
58
81
|
expect(post.pinned).toBe(1);
|
|
59
82
|
expect(post.path).toBe("my-link");
|
|
60
83
|
expect(post.url).toBe("https://example.com/source");
|
|
@@ -63,10 +86,27 @@ describe("PostService", () => {
|
|
|
63
86
|
expect(post.bodyHtml).toContain("<h1>");
|
|
64
87
|
});
|
|
65
88
|
|
|
66
|
-
it("renders
|
|
89
|
+
it("renders Tiptap JSON body to HTML", async () => {
|
|
90
|
+
const body = JSON.stringify({
|
|
91
|
+
type: "doc",
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "paragraph",
|
|
95
|
+
content: [
|
|
96
|
+
{ type: "text", text: "This is " },
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
marks: [{ type: "bold" }],
|
|
100
|
+
text: "bold",
|
|
101
|
+
},
|
|
102
|
+
{ type: "text", text: " text" },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
67
107
|
const post = await postService.create({
|
|
68
108
|
format: "note",
|
|
69
|
-
body
|
|
109
|
+
body,
|
|
70
110
|
});
|
|
71
111
|
|
|
72
112
|
expect(post.bodyHtml).toContain("<strong>bold</strong>");
|
|
@@ -267,26 +307,60 @@ describe("PostService", () => {
|
|
|
267
307
|
expect(published[0]?.status).toBe("published");
|
|
268
308
|
});
|
|
269
309
|
|
|
270
|
-
it("filters by
|
|
310
|
+
it("filters by visibility", async () => {
|
|
271
311
|
await postService.create({
|
|
272
312
|
format: "note",
|
|
273
313
|
body: "featured post",
|
|
274
|
-
|
|
314
|
+
visibility: "featured",
|
|
275
315
|
});
|
|
276
316
|
await postService.create({
|
|
277
317
|
format: "note",
|
|
278
318
|
body: "normal post",
|
|
279
319
|
});
|
|
320
|
+
await postService.create({
|
|
321
|
+
format: "note",
|
|
322
|
+
body: "unlisted post",
|
|
323
|
+
visibility: "unlisted",
|
|
324
|
+
});
|
|
280
325
|
|
|
281
|
-
const featured = await postService.list({
|
|
326
|
+
const featured = await postService.list({ visibility: "featured" });
|
|
282
327
|
expect(featured).toHaveLength(1);
|
|
283
|
-
expect(featured[0]?.
|
|
328
|
+
expect(featured[0]?.visibility).toBe("featured");
|
|
284
329
|
expect(featured[0]?.body).toBe("featured post");
|
|
285
330
|
|
|
286
|
-
const
|
|
287
|
-
expect(
|
|
288
|
-
expect(
|
|
289
|
-
expect(
|
|
331
|
+
const listed = await postService.list({ visibility: "listed" });
|
|
332
|
+
expect(listed).toHaveLength(1);
|
|
333
|
+
expect(listed[0]?.visibility).toBe("listed");
|
|
334
|
+
expect(listed[0]?.body).toBe("normal post");
|
|
335
|
+
|
|
336
|
+
const unlisted = await postService.list({ visibility: "unlisted" });
|
|
337
|
+
expect(unlisted).toHaveLength(1);
|
|
338
|
+
expect(unlisted[0]?.visibility).toBe("unlisted");
|
|
339
|
+
expect(unlisted[0]?.body).toBe("unlisted post");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("excludes unlisted posts when requested", async () => {
|
|
343
|
+
await postService.create({
|
|
344
|
+
format: "note",
|
|
345
|
+
body: "listed post",
|
|
346
|
+
});
|
|
347
|
+
await postService.create({
|
|
348
|
+
format: "note",
|
|
349
|
+
body: "unlisted post",
|
|
350
|
+
visibility: "unlisted",
|
|
351
|
+
});
|
|
352
|
+
await postService.create({
|
|
353
|
+
format: "note",
|
|
354
|
+
body: "featured post",
|
|
355
|
+
visibility: "featured",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const posts = await postService.list({ excludeUnlisted: true });
|
|
359
|
+
expect(posts).toHaveLength(2);
|
|
360
|
+
expect(posts.map((p) => p.body).sort()).toEqual([
|
|
361
|
+
"featured post",
|
|
362
|
+
"listed post",
|
|
363
|
+
]);
|
|
290
364
|
});
|
|
291
365
|
|
|
292
366
|
it("filters by pinned", async () => {
|
|
@@ -426,15 +500,15 @@ describe("PostService", () => {
|
|
|
426
500
|
expect(count).toBe(1);
|
|
427
501
|
});
|
|
428
502
|
|
|
429
|
-
it("filters by
|
|
503
|
+
it("filters by visibility", async () => {
|
|
430
504
|
await postService.create({
|
|
431
505
|
format: "note",
|
|
432
506
|
body: "featured",
|
|
433
|
-
|
|
507
|
+
visibility: "featured",
|
|
434
508
|
});
|
|
435
509
|
await postService.create({ format: "note", body: "normal" });
|
|
436
510
|
|
|
437
|
-
const count = await postService.count({
|
|
511
|
+
const count = await postService.count({ visibility: "featured" });
|
|
438
512
|
expect(count).toBe(1);
|
|
439
513
|
});
|
|
440
514
|
|
|
@@ -470,15 +544,32 @@ describe("PostService", () => {
|
|
|
470
544
|
it("updates post body", async () => {
|
|
471
545
|
const post = await postService.create({
|
|
472
546
|
format: "note",
|
|
473
|
-
body:
|
|
547
|
+
body: JSON.stringify({
|
|
548
|
+
type: "doc",
|
|
549
|
+
content: [
|
|
550
|
+
{
|
|
551
|
+
type: "paragraph",
|
|
552
|
+
content: [{ type: "text", text: "original" }],
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
}),
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const updatedBody = JSON.stringify({
|
|
559
|
+
type: "doc",
|
|
560
|
+
content: [
|
|
561
|
+
{
|
|
562
|
+
type: "paragraph",
|
|
563
|
+
content: [{ type: "text", text: "updated content" }],
|
|
564
|
+
},
|
|
565
|
+
],
|
|
474
566
|
});
|
|
475
|
-
|
|
476
567
|
const updated = await postService.update(post.id, {
|
|
477
|
-
body:
|
|
568
|
+
body: updatedBody,
|
|
478
569
|
});
|
|
479
570
|
|
|
480
571
|
expect(updated).not.toBeNull();
|
|
481
|
-
expect(updated?.body).toBe(
|
|
572
|
+
expect(updated?.body).toBe(updatedBody);
|
|
482
573
|
expect(updated?.bodyHtml).toContain("updated content");
|
|
483
574
|
});
|
|
484
575
|
|
|
@@ -547,19 +638,19 @@ describe("PostService", () => {
|
|
|
547
638
|
expect(updated?.updatedAt).toBeGreaterThanOrEqual(originalUpdatedAt);
|
|
548
639
|
});
|
|
549
640
|
|
|
550
|
-
it("updates
|
|
641
|
+
it("updates visibility", async () => {
|
|
551
642
|
const post = await postService.create({
|
|
552
643
|
format: "note",
|
|
553
644
|
body: "test",
|
|
554
645
|
});
|
|
555
646
|
|
|
556
|
-
expect(post.
|
|
647
|
+
expect(post.visibility).toBe("listed");
|
|
557
648
|
|
|
558
649
|
const updated = await postService.update(post.id, {
|
|
559
|
-
|
|
650
|
+
visibility: "featured",
|
|
560
651
|
});
|
|
561
652
|
|
|
562
|
-
expect(updated?.
|
|
653
|
+
expect(updated?.visibility).toBe("featured");
|
|
563
654
|
});
|
|
564
655
|
|
|
565
656
|
it("updates pinned flag", async () => {
|
|
@@ -723,11 +814,11 @@ describe("PostService", () => {
|
|
|
723
814
|
expect(reply.status).toBe("draft");
|
|
724
815
|
});
|
|
725
816
|
|
|
726
|
-
it("inherits
|
|
817
|
+
it("inherits visibility from root post", async () => {
|
|
727
818
|
const root = await postService.create({
|
|
728
819
|
format: "note",
|
|
729
820
|
body: "root",
|
|
730
|
-
|
|
821
|
+
visibility: "featured",
|
|
731
822
|
});
|
|
732
823
|
const reply = await postService.create({
|
|
733
824
|
format: "note",
|
|
@@ -735,7 +826,7 @@ describe("PostService", () => {
|
|
|
735
826
|
replyToId: root.id,
|
|
736
827
|
});
|
|
737
828
|
|
|
738
|
-
expect(reply.
|
|
829
|
+
expect(reply.visibility).toBe("featured");
|
|
739
830
|
});
|
|
740
831
|
|
|
741
832
|
it("getThread returns all posts in a thread", async () => {
|
|
@@ -797,7 +888,7 @@ describe("PostService", () => {
|
|
|
797
888
|
}
|
|
798
889
|
});
|
|
799
890
|
|
|
800
|
-
it("cascades
|
|
891
|
+
it("cascades visibility changes from root to thread", async () => {
|
|
801
892
|
const root = await postService.create({
|
|
802
893
|
format: "note",
|
|
803
894
|
body: "root",
|
|
@@ -808,11 +899,11 @@ describe("PostService", () => {
|
|
|
808
899
|
replyToId: root.id,
|
|
809
900
|
});
|
|
810
901
|
|
|
811
|
-
await postService.update(root.id, {
|
|
902
|
+
await postService.update(root.id, { visibility: "featured" });
|
|
812
903
|
|
|
813
904
|
const thread = await postService.getThread(root.id);
|
|
814
905
|
for (const post of thread) {
|
|
815
|
-
expect(post.
|
|
906
|
+
expect(post.visibility).toBe("featured");
|
|
816
907
|
}
|
|
817
908
|
});
|
|
818
909
|
});
|
|
@@ -102,7 +102,7 @@ describe("SettingsService", () => {
|
|
|
102
102
|
siteFooter: "",
|
|
103
103
|
siteLanguage: "en",
|
|
104
104
|
homeDefaultView: "latest",
|
|
105
|
-
headerNavMaxVisible: "
|
|
105
|
+
headerNavMaxVisible: "2",
|
|
106
106
|
timeZone: "UTC",
|
|
107
107
|
};
|
|
108
108
|
|
|
@@ -154,10 +154,10 @@ describe("SettingsService", () => {
|
|
|
154
154
|
expect(await settingsService.get("HOME_DEFAULT_VIEW")).toBe("featured");
|
|
155
155
|
});
|
|
156
156
|
|
|
157
|
-
it("removes HEADER_NAV_MAX_VISIBLE when set to default (
|
|
157
|
+
it("removes HEADER_NAV_MAX_VISIBLE when set to default (2)", async () => {
|
|
158
158
|
await settingsService.set("HEADER_NAV_MAX_VISIBLE", "5");
|
|
159
159
|
await settingsService.updateGeneral(
|
|
160
|
-
{ ...defaults, headerNavMaxVisible: "
|
|
160
|
+
{ ...defaults, headerNavMaxVisible: "2" },
|
|
161
161
|
{ oldLanguage: "en", fallbackSiteName: "Jant" },
|
|
162
162
|
);
|
|
163
163
|
|
package/src/services/page.ts
CHANGED
|
@@ -27,10 +27,23 @@ export interface PageService {
|
|
|
27
27
|
delete(id: number): Promise<boolean>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** Check if an error is a SQLite UNIQUE constraint violation
|
|
30
|
+
/** Check if an error (or any of its causes) is a SQLite UNIQUE constraint violation */
|
|
31
31
|
function isUniqueConstraintError(err: unknown): boolean {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
let current: unknown = err;
|
|
33
|
+
while (current) {
|
|
34
|
+
const msg = String(current);
|
|
35
|
+
if (
|
|
36
|
+
msg.includes("UNIQUE constraint") ||
|
|
37
|
+
msg.includes("SQLITE_CONSTRAINT")
|
|
38
|
+
) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
current =
|
|
42
|
+
current instanceof Error && current.cause !== current
|
|
43
|
+
? current.cause
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export function createPageService(
|