@jant/core 0.6.11 → 0.6.13
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/bin/commands/import-site.js +47 -30
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/app-CWJFPjuR.js +6 -0
- package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
- package/dist/client/_assets/client-DCrcHVJl.css +2 -0
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
- package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
- package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +76 -2
- package/src/__tests__/vite-dev-plugins.test.ts +34 -0
- package/src/client/__tests__/compose-launch.test.ts +50 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
- package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +10 -2
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +382 -152
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -10
- package/src/client/components/settings-types.ts +30 -0
- package/src/client/compose-launch.ts +30 -0
- package/src/client/compose-shortcuts.ts +46 -3
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/client-auth.ts +0 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +22 -6
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +22 -6
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +77 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +85 -28
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +79 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/feed.ts +63 -3
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/navigation.ts +5 -22
- package/src/lib/post-display.ts +14 -8
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/preset.css +60 -18
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +48 -3
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +231 -1
- package/src/services/__tests__/post-timeline.test.ts +175 -2
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +7 -16
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
- package/src/services/export-theme/styles/main.css +46 -14
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/index.ts +27 -17
- package/src/services/navigation.ts +186 -1
- package/src/services/post.ts +103 -63
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +28 -10
- package/src/styles/ui.css +349 -13
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/types/views.ts +13 -0
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/src/ui/layouts/SiteLayout.tsx +249 -216
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
- package/src/client/nav-manager-bridge.ts +0 -54
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
getPathCommandSearchQuery,
|
|
6
|
+
getPathCommandTarget,
|
|
7
|
+
} from "../jant-command-palette.js";
|
|
8
|
+
import type { JantCommandPalette } from "../jant-command-palette.js";
|
|
9
|
+
|
|
10
|
+
function mockPaletteApi() {
|
|
11
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
12
|
+
new Response(JSON.stringify({ items: [] }), {
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function renderPalette(query: string) {
|
|
19
|
+
mockPaletteApi();
|
|
20
|
+
const el = document.createElement(
|
|
21
|
+
"jant-command-palette",
|
|
22
|
+
) as JantCommandPalette;
|
|
23
|
+
el._open = true;
|
|
24
|
+
el._query = query;
|
|
25
|
+
document.body.appendChild(el);
|
|
26
|
+
await Promise.resolve();
|
|
27
|
+
await el.updateComplete;
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resultTitles(root: globalThis.Element): string[] {
|
|
32
|
+
return Array.from(
|
|
33
|
+
root.querySelectorAll<HTMLElement>(".command-palette-result-title"),
|
|
34
|
+
).map((item) => item.textContent?.trim() ?? "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("JantCommandPalette slash path mode", () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
document.body.innerHTML = "";
|
|
41
|
+
globalThis.localStorage.clear();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows go-to-path first and search second for slash queries", async () => {
|
|
45
|
+
const el = await renderPalette("/draft-123");
|
|
46
|
+
|
|
47
|
+
expect(resultTitles(el)).toEqual([
|
|
48
|
+
"Go to /draft-123",
|
|
49
|
+
'Search for "draft-123"',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("normalizes repeated leading slashes into a local path", () => {
|
|
54
|
+
expect(getPathCommandTarget("//example.com")).toBe("/example.com");
|
|
55
|
+
expect(getPathCommandSearchQuery("//example.com")).toBe("example.com");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("accepts fullwidth slash input", async () => {
|
|
59
|
+
const el = await renderPalette("/notes");
|
|
60
|
+
|
|
61
|
+
expect(resultTitles(el)).toEqual(["Go to /notes", 'Search for "notes"']);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -644,6 +644,9 @@ describe("JantComposeDialog", () => {
|
|
|
644
644
|
).toBeNull();
|
|
645
645
|
// ...it sits inline above the reply editor instead.
|
|
646
646
|
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
647
|
+
expect(
|
|
648
|
+
el.querySelector(".compose-thread-layout.compose-reply-compose-layout"),
|
|
649
|
+
).not.toBeNull();
|
|
647
650
|
});
|
|
648
651
|
|
|
649
652
|
it("shows an Edit title with the format selector above the post when editing a reply", async () => {
|
|
@@ -3065,6 +3068,36 @@ describe("JantComposeDialog", () => {
|
|
|
3065
3068
|
expect(css).toMatch(
|
|
3066
3069
|
/\.compose-editor-row\s+\.compose-attachments-dock\s*\{[\s\S]*max-height:\s*min\(240px,\s*34dvh\);[\s\S]*overflow-y:\s*auto;/,
|
|
3067
3070
|
);
|
|
3071
|
+
expect(css).toMatch(
|
|
3072
|
+
/\.compose-reply-compose-layout\s+\.compose-editor-row\s+\.compose-attachments-dock\s*\{[\s\S]*--compose-reply-attachment-width:\s*96px;[\s\S]*--compose-reply-attachment-height:\s*72px;[\s\S]*max-height:\s*min\(116px,\s*18dvh\);/,
|
|
3073
|
+
);
|
|
3074
|
+
expect(css).toMatch(
|
|
3075
|
+
/\.compose-reply-compose-layout[\s\S]*\.compose-attachment:only-child[\s\S]*\.compose-attachment-img,[\s\S]*\.compose-reply-compose-layout[\s\S]*\.compose-attachment:not\(:only-child\)[\s\S]*\.compose-attachment-img\s*\{[\s\S]*height:\s*var\(--compose-reply-attachment-height\);[\s\S]*object-fit:\s*cover;/,
|
|
3076
|
+
);
|
|
3077
|
+
expect(css).toMatch(
|
|
3078
|
+
/\.compose-reply-compose-layout\s+\.compose-thread-post-header\s*\+\s*\.compose-body\s*\{[\s\S]*padding-top:\s*12px;/,
|
|
3079
|
+
);
|
|
3080
|
+
expect(css).toMatch(
|
|
3081
|
+
/\.compose-dialog,[\s\S]*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-quote-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-leading:\s*1\.32;/,
|
|
3082
|
+
);
|
|
3083
|
+
expect(css).toMatch(
|
|
3084
|
+
/\.compose-quote-text\s*\{[\s\S]*font-size:\s*var\(--compose-quote-input-size\);[\s\S]*line-height:\s*var\(--compose-quote-input-leading\);/,
|
|
3085
|
+
);
|
|
3086
|
+
expect(css).toMatch(
|
|
3087
|
+
/\.compose-reply-compose-layout\s*\{[\s\S]*--compose-title-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-size:\s*calc\(var\(--type-content-body\) \* 1\.06\);[\s\S]*--compose-quote-input-leading:\s*1\.42;[\s\S]*--compose-inline-input-size:\s*var\(--type-base\);/,
|
|
3088
|
+
);
|
|
3089
|
+
expect(css).toMatch(
|
|
3090
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;[\s\S]*padding:\s*24px 18px 18px;[\s\S]*border-radius:\s*0\.95rem;/,
|
|
3091
|
+
);
|
|
3092
|
+
expect(css).toMatch(
|
|
3093
|
+
/\.compose-reply-compose-layout\s+\.compose-link-url-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;/,
|
|
3094
|
+
);
|
|
3095
|
+
expect(css).toMatch(
|
|
3096
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-text\s*\{[\s\S]*min-height:\s*6\.5rem;[\s\S]*padding-top:\s*0\.35rem;/,
|
|
3097
|
+
);
|
|
3098
|
+
expect(css).toMatch(
|
|
3099
|
+
/\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s*\{[\s\S]*font-size:\s*var\(--type-content-body\);/,
|
|
3100
|
+
);
|
|
3068
3101
|
});
|
|
3069
3102
|
|
|
3070
3103
|
it("remove button clears attachment", async () => {
|
|
@@ -21,6 +21,22 @@ function editorOf(el: JantComposeEditor): Editor {
|
|
|
21
21
|
|
|
22
22
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
23
|
|
|
24
|
+
function dispatchPaste(editor: Editor, html: string, text = "") {
|
|
25
|
+
const event = new Event("paste", {
|
|
26
|
+
bubbles: true,
|
|
27
|
+
cancelable: true,
|
|
28
|
+
}) as Event & { clipboardData: unknown };
|
|
29
|
+
event.clipboardData = {
|
|
30
|
+
getData: (type: string) =>
|
|
31
|
+
type === "text/html" ? html : type === "text/plain" ? text : "",
|
|
32
|
+
files: [],
|
|
33
|
+
items: [],
|
|
34
|
+
types: ["text/html", "text/plain"],
|
|
35
|
+
};
|
|
36
|
+
editor.commands.focus();
|
|
37
|
+
editor.view.dom.dispatchEvent(event);
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
beforeEach(() => {
|
|
25
41
|
const container = document.createElement("div");
|
|
26
42
|
container.id = "toast-container";
|
|
@@ -48,7 +64,10 @@ describe("compose editor: rehost failure notice", () => {
|
|
|
48
64
|
document.body.appendChild(el);
|
|
49
65
|
await el.updateComplete;
|
|
50
66
|
|
|
51
|
-
|
|
67
|
+
dispatchPaste(
|
|
68
|
+
editorOf(el),
|
|
69
|
+
'<p><img src="https://ext.example/blocked.png"></p>',
|
|
70
|
+
);
|
|
52
71
|
|
|
53
72
|
// Let the rehost fire, the sideload fail, then the debounce window elapse.
|
|
54
73
|
await wait(50);
|
|
@@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
4
4
|
vi.mock("../../upload-with-metadata.js", () => ({
|
|
5
5
|
uploadWithMetadata: vi.fn(),
|
|
6
6
|
}));
|
|
7
|
-
import type { Editor } from "@tiptap/core";
|
|
7
|
+
import type { Editor, JSONContent } from "@tiptap/core";
|
|
8
8
|
import type { Slice } from "@tiptap/pm/model";
|
|
9
9
|
import * as ProseMirrorView from "@tiptap/pm/view";
|
|
10
10
|
import type { ComposeLabels } from "../compose-types.js";
|
|
@@ -301,6 +301,25 @@ async function createElement(
|
|
|
301
301
|
return el;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
function tiptapDoc(...content: JSONContent[]): JSONContent {
|
|
305
|
+
return { type: "doc", content };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function tiptapHeading(level: number, text: string): JSONContent {
|
|
309
|
+
return {
|
|
310
|
+
type: "heading",
|
|
311
|
+
attrs: { level },
|
|
312
|
+
content: [{ type: "text", text }],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function tiptapParagraph(text: string): JSONContent {
|
|
317
|
+
return {
|
|
318
|
+
type: "paragraph",
|
|
319
|
+
content: [{ type: "text", text }],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
304
323
|
async function toggleEmojiPicker(el: JantComposeEditor) {
|
|
305
324
|
(
|
|
306
325
|
el as unknown as {
|
|
@@ -905,6 +924,77 @@ describe("JantComposeEditor", () => {
|
|
|
905
924
|
expect(el.getData().title).toBe("My Title");
|
|
906
925
|
});
|
|
907
926
|
|
|
927
|
+
it("promotes a leading H1 to the note title", async () => {
|
|
928
|
+
const el = await createElement("note");
|
|
929
|
+
const editor = requireEditor(el);
|
|
930
|
+
editor.commands.setContent(
|
|
931
|
+
tiptapDoc(tiptapHeading(1, "My Markdown Title"), tiptapParagraph("Body")),
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
el.promoteLeadingH1Title({ force: true });
|
|
935
|
+
await el.updateComplete;
|
|
936
|
+
|
|
937
|
+
const data = el.getData();
|
|
938
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
939
|
+
|
|
940
|
+
expect(data.title).toBe("My Markdown Title");
|
|
941
|
+
expect(el._showTitle).toBe(true);
|
|
942
|
+
expect(body.content?.[0]).toEqual(tiptapParagraph("Body"));
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it("keeps a leading H1 in the body while the cursor is still in it", async () => {
|
|
946
|
+
const el = await createElement("note");
|
|
947
|
+
const editor = requireEditor(el);
|
|
948
|
+
|
|
949
|
+
editor.commands.setContent(tiptapDoc(tiptapHeading(1, "Draft title")));
|
|
950
|
+
await el.updateComplete;
|
|
951
|
+
|
|
952
|
+
const data = el.getData();
|
|
953
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
954
|
+
|
|
955
|
+
expect(data.title).toBe("");
|
|
956
|
+
expect(el._showTitle).toBe(false);
|
|
957
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Draft title"));
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("does not overwrite an explicit note title with a leading H1", async () => {
|
|
961
|
+
const el = await createElement("note");
|
|
962
|
+
const editor = requireEditor(el);
|
|
963
|
+
el._title = "Manual title";
|
|
964
|
+
el._showTitle = true;
|
|
965
|
+
|
|
966
|
+
editor.commands.setContent(
|
|
967
|
+
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
el.promoteLeadingH1Title({ force: true });
|
|
971
|
+
await el.updateComplete;
|
|
972
|
+
|
|
973
|
+
const data = el.getData();
|
|
974
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
975
|
+
|
|
976
|
+
expect(data.title).toBe("Manual title");
|
|
977
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Markdown Title"));
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("does not promote a leading H1 for link posts", async () => {
|
|
981
|
+
const el = await createElement("link");
|
|
982
|
+
const editor = requireEditor(el);
|
|
983
|
+
|
|
984
|
+
editor.commands.setContent(
|
|
985
|
+
tiptapDoc(tiptapHeading(1, "Markdown Title"), tiptapParagraph("Body")),
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
el.promoteLeadingH1Title({ force: true });
|
|
989
|
+
await el.updateComplete;
|
|
990
|
+
|
|
991
|
+
const data = el.getData();
|
|
992
|
+
const body = JSON.parse(data.body) as JSONContent;
|
|
993
|
+
|
|
994
|
+
expect(data.title).toBe("");
|
|
995
|
+
expect(body.content?.[0]).toEqual(tiptapHeading(1, "Markdown Title"));
|
|
996
|
+
});
|
|
997
|
+
|
|
908
998
|
it("omits rating when the rating control is hidden", async () => {
|
|
909
999
|
const el = await createElement("note");
|
|
910
1000
|
el._rating = 3;
|
|
@@ -1016,7 +1106,9 @@ describe("JantComposeEditor", () => {
|
|
|
1016
1106
|
image.dispatchEvent(new Event("error"));
|
|
1017
1107
|
await el.updateComplete;
|
|
1018
1108
|
|
|
1019
|
-
|
|
1109
|
+
const fallback = el.querySelector("[data-preview-failed='image']");
|
|
1110
|
+
expect(fallback).not.toBeNull();
|
|
1111
|
+
expect(fallback?.textContent).toContain("Image unavailable");
|
|
1020
1112
|
expect(el.querySelector(".compose-attachment-remove")).not.toBeNull();
|
|
1021
1113
|
expect(el.querySelector(".compose-attachment-img")).toBeNull();
|
|
1022
1114
|
});
|
|
@@ -15,7 +15,11 @@ vi.mock("sortablejs", () => ({
|
|
|
15
15
|
},
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
NavManagerItem,
|
|
20
|
+
NavManagerLabels,
|
|
21
|
+
NavManagerSuggestedLink,
|
|
22
|
+
} from "../nav-manager-types.js";
|
|
19
23
|
import "../jant-nav-manager.js";
|
|
20
24
|
import type { JantNavManager } from "../jant-nav-manager.js";
|
|
21
25
|
|
|
@@ -43,6 +47,10 @@ const labels: NavManagerLabels = {
|
|
|
43
47
|
addLinkDescription: "Add a custom nav link.",
|
|
44
48
|
urlPlaceholder: "/about",
|
|
45
49
|
labelAndUrlRequired: "Add a label and URL.",
|
|
50
|
+
suggestedLinks: "Suggested links",
|
|
51
|
+
suggestedLinksDescription: "Add common destinations.",
|
|
52
|
+
addSuggestedLink: "Add",
|
|
53
|
+
suggestedLinkAdded: "Link added to navigation.",
|
|
46
54
|
headerSection: "Header",
|
|
47
55
|
moreSection: "More",
|
|
48
56
|
moreEmptyHint: "Move links here to hide them under More.",
|
|
@@ -84,6 +92,58 @@ const items: NavManagerItem[] = [
|
|
|
84
92
|
},
|
|
85
93
|
];
|
|
86
94
|
|
|
95
|
+
const suggestedLinks: NavManagerSuggestedLink[] = [
|
|
96
|
+
{
|
|
97
|
+
key: "now",
|
|
98
|
+
label: "Now",
|
|
99
|
+
url: "/now",
|
|
100
|
+
targetType: "collection",
|
|
101
|
+
targetLabel: "Collection",
|
|
102
|
+
navItemType: "collection",
|
|
103
|
+
collectionId: "col_now",
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function renderHeaderFragment(label: string): string {
|
|
108
|
+
return `
|
|
109
|
+
<header class="site-header" data-site-header-fragment="header">
|
|
110
|
+
<div class="site-header-inner">
|
|
111
|
+
<div class="site-header-top">
|
|
112
|
+
<a href="/" class="site-logo">Test Site</a>
|
|
113
|
+
<nav class="site-header-nav" aria-label="Primary">
|
|
114
|
+
<a href="/now" class="site-header-link">${label}</a>
|
|
115
|
+
</nav>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
class="site-header-hamburger"
|
|
119
|
+
aria-controls="site-nav-drawer"
|
|
120
|
+
aria-expanded="false"
|
|
121
|
+
></button>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</header>
|
|
125
|
+
<div
|
|
126
|
+
class="site-nav-drawer-backdrop"
|
|
127
|
+
data-site-header-fragment="drawer-backdrop"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
></div>
|
|
130
|
+
<div
|
|
131
|
+
id="site-nav-drawer"
|
|
132
|
+
class="site-nav-drawer"
|
|
133
|
+
data-site-header-fragment="drawer"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
inert
|
|
136
|
+
>
|
|
137
|
+
<button class="site-nav-drawer-close" type="button"></button>
|
|
138
|
+
<a href="/now" class="site-nav-drawer-link">${label}</a>
|
|
139
|
+
</div>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function installCurrentHeaderFragment(label = "Old"): void {
|
|
144
|
+
document.body.insertAdjacentHTML("afterbegin", renderHeaderFragment(label));
|
|
145
|
+
}
|
|
146
|
+
|
|
87
147
|
function requireElement<T>(value: T | null, message: string): T {
|
|
88
148
|
if (!value) {
|
|
89
149
|
throw new Error(message);
|
|
@@ -128,11 +188,18 @@ function getSortableOptions(
|
|
|
128
188
|
return options as Record<string, ((event: unknown) => void) | undefined>;
|
|
129
189
|
}
|
|
130
190
|
|
|
191
|
+
async function flushAsyncWork(): Promise<void> {
|
|
192
|
+
await Promise.resolve();
|
|
193
|
+
await Promise.resolve();
|
|
194
|
+
await Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
|
|
131
197
|
async function createElement(): Promise<JantNavManager> {
|
|
132
198
|
const el = document.createElement("jant-nav-manager") as JantNavManager;
|
|
133
199
|
el.labels = labels;
|
|
134
200
|
el.items = items;
|
|
135
201
|
el.systemNavItems = [];
|
|
202
|
+
el.suggestedLinks = [];
|
|
136
203
|
el.siteName = "Test Site";
|
|
137
204
|
document.body.appendChild(el);
|
|
138
205
|
await el.updateComplete;
|
|
@@ -149,6 +216,7 @@ describe("JantNavManager", () => {
|
|
|
149
216
|
vi.fn().mockResolvedValue({
|
|
150
217
|
ok: true,
|
|
151
218
|
status: 200,
|
|
219
|
+
json: async () => ({ ...items[1] }),
|
|
152
220
|
}),
|
|
153
221
|
);
|
|
154
222
|
});
|
|
@@ -188,8 +256,7 @@ describe("JantNavManager", () => {
|
|
|
188
256
|
});
|
|
189
257
|
|
|
190
258
|
await el.updateComplete;
|
|
191
|
-
await
|
|
192
|
-
await Promise.resolve();
|
|
259
|
+
await flushAsyncWork();
|
|
193
260
|
|
|
194
261
|
expect(getListIds(headerList)).toEqual(["nav-1"]);
|
|
195
262
|
expect(getListIds(moreList)).toEqual(["nav-2", "nav-3"]);
|
|
@@ -213,6 +280,9 @@ describe("JantNavManager", () => {
|
|
|
213
280
|
"/api/nav-items/nav-2/move",
|
|
214
281
|
expect.objectContaining({
|
|
215
282
|
method: "PUT",
|
|
283
|
+
headers: expect.objectContaining({
|
|
284
|
+
"X-Jant-Site-Header": "include",
|
|
285
|
+
}),
|
|
216
286
|
body: JSON.stringify({
|
|
217
287
|
after: null,
|
|
218
288
|
before: "nav-3",
|
|
@@ -251,10 +321,84 @@ describe("JantNavManager", () => {
|
|
|
251
321
|
expect(popover.getAttribute("aria-hidden")).toBe("true");
|
|
252
322
|
});
|
|
253
323
|
|
|
254
|
-
it("
|
|
324
|
+
it("adds a suggested collection link through the nav items API", async () => {
|
|
255
325
|
const el = await createElement();
|
|
256
|
-
|
|
257
|
-
el.
|
|
326
|
+
el.suggestedLinks = suggestedLinks;
|
|
327
|
+
el.requestUpdate();
|
|
328
|
+
await el.updateComplete;
|
|
329
|
+
|
|
330
|
+
const addButton = requireElement(
|
|
331
|
+
el.querySelector<HTMLButtonElement>(".nav-suggestion-item button"),
|
|
332
|
+
"expected suggested link add button",
|
|
333
|
+
);
|
|
334
|
+
expect(el.textContent).toContain("Now");
|
|
335
|
+
expect(el.textContent).toContain("/now · Collection");
|
|
336
|
+
|
|
337
|
+
const created: NavManagerItem = {
|
|
338
|
+
id: "nav-now",
|
|
339
|
+
type: "collection",
|
|
340
|
+
collectionId: "col_now",
|
|
341
|
+
label: "Now",
|
|
342
|
+
url: "/now",
|
|
343
|
+
placement: "header",
|
|
344
|
+
};
|
|
345
|
+
installCurrentHeaderFragment("Old");
|
|
346
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
347
|
+
ok: true,
|
|
348
|
+
status: 201,
|
|
349
|
+
json: async () => ({
|
|
350
|
+
...created,
|
|
351
|
+
headerHtml: renderHeaderFragment("Now"),
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
355
|
+
|
|
356
|
+
addButton.click();
|
|
357
|
+
await flushAsyncWork();
|
|
358
|
+
await el.updateComplete;
|
|
359
|
+
|
|
360
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
361
|
+
"/api/nav-items",
|
|
362
|
+
expect.objectContaining({
|
|
363
|
+
method: "POST",
|
|
364
|
+
headers: expect.objectContaining({
|
|
365
|
+
"X-Jant-Site-Header": "include",
|
|
366
|
+
}),
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
type: "collection",
|
|
369
|
+
collectionId: "col_now",
|
|
370
|
+
placement: "header",
|
|
371
|
+
}),
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
expect(
|
|
375
|
+
getListIds(
|
|
376
|
+
requireElement(
|
|
377
|
+
el.querySelector<HTMLElement>("#nav-items-header"),
|
|
378
|
+
"expected header nav list",
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
).toContain("nav-now");
|
|
382
|
+
expect(el.querySelector(".nav-suggestion-item")).toBeNull();
|
|
383
|
+
expect(
|
|
384
|
+
document
|
|
385
|
+
.querySelector<HTMLElement>('[data-site-header-fragment="header"]')
|
|
386
|
+
?.textContent?.trim(),
|
|
387
|
+
).toContain("Now");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("confirms before deleting a navigation item", async () => {
|
|
391
|
+
const el = await createElement();
|
|
392
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
393
|
+
ok: true,
|
|
394
|
+
status: 200,
|
|
395
|
+
json: async () => ({
|
|
396
|
+
success: true,
|
|
397
|
+
headerHtml: renderHeaderFragment("Links"),
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
401
|
+
installCurrentHeaderFragment("About");
|
|
258
402
|
|
|
259
403
|
(
|
|
260
404
|
el as unknown as { _editingId: string | null; requestUpdate: () => void }
|
|
@@ -267,7 +411,7 @@ describe("JantNavManager", () => {
|
|
|
267
411
|
"expected nav delete button",
|
|
268
412
|
);
|
|
269
413
|
deleteButton.click();
|
|
270
|
-
await
|
|
414
|
+
await flushAsyncWork();
|
|
271
415
|
|
|
272
416
|
const host = requireElement(
|
|
273
417
|
document.querySelector<HTMLElement>("jant-confirm-dialog"),
|
|
@@ -280,19 +424,37 @@ describe("JantNavManager", () => {
|
|
|
280
424
|
"expected confirm button",
|
|
281
425
|
);
|
|
282
426
|
confirmButton.click();
|
|
283
|
-
await
|
|
284
|
-
await
|
|
427
|
+
await flushAsyncWork();
|
|
428
|
+
await el.updateComplete;
|
|
285
429
|
|
|
286
|
-
expect(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
430
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
431
|
+
"/api/nav-items/nav-1",
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
method: "DELETE",
|
|
434
|
+
headers: expect.objectContaining({
|
|
435
|
+
"X-Jant-Site-Header": "include",
|
|
436
|
+
}),
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
expect(
|
|
440
|
+
getListIds(
|
|
441
|
+
requireElement(
|
|
442
|
+
el.querySelector<HTMLElement>("#nav-items-header"),
|
|
443
|
+
"expected header nav list",
|
|
444
|
+
),
|
|
445
|
+
),
|
|
446
|
+
).toEqual(["nav-2"]);
|
|
447
|
+
expect(
|
|
448
|
+
document
|
|
449
|
+
.querySelector<HTMLElement>('[data-site-header-fragment="header"]')
|
|
450
|
+
?.textContent?.trim(),
|
|
451
|
+
).toContain("Links");
|
|
290
452
|
});
|
|
291
453
|
|
|
292
|
-
it("does not
|
|
454
|
+
it("does not delete when confirmation is canceled", async () => {
|
|
293
455
|
const el = await createElement();
|
|
294
|
-
const
|
|
295
|
-
|
|
456
|
+
const fetchMock = vi.fn();
|
|
457
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
296
458
|
|
|
297
459
|
(
|
|
298
460
|
el as unknown as { _editingId: string | null; requestUpdate: () => void }
|
|
@@ -305,7 +467,7 @@ describe("JantNavManager", () => {
|
|
|
305
467
|
"expected nav delete button",
|
|
306
468
|
);
|
|
307
469
|
deleteButton.click();
|
|
308
|
-
await
|
|
470
|
+
await flushAsyncWork();
|
|
309
471
|
|
|
310
472
|
const host = requireElement(
|
|
311
473
|
document.querySelector<HTMLElement>("jant-confirm-dialog"),
|
|
@@ -318,9 +480,8 @@ describe("JantNavManager", () => {
|
|
|
318
480
|
"expected cancel button",
|
|
319
481
|
);
|
|
320
482
|
cancelButton.click();
|
|
321
|
-
await
|
|
322
|
-
await Promise.resolve();
|
|
483
|
+
await flushAsyncWork();
|
|
323
484
|
|
|
324
|
-
expect(
|
|
485
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
325
486
|
});
|
|
326
487
|
});
|
|
@@ -32,6 +32,12 @@ const labels: SettingsLabels = {
|
|
|
32
32
|
uploadError: "Upload failed.",
|
|
33
33
|
general: "General",
|
|
34
34
|
site: "Site",
|
|
35
|
+
aboutPage: "About page",
|
|
36
|
+
aboutPagePrompt: "Want to write a fuller introduction?",
|
|
37
|
+
aboutPageConflict:
|
|
38
|
+
"/about is already used. Rename that item before creating an About page.",
|
|
39
|
+
createAboutPage: "Create About page",
|
|
40
|
+
editAboutPage: "Edit About page",
|
|
35
41
|
languageAndTime: "Language & Time",
|
|
36
42
|
home: "Home",
|
|
37
43
|
search: "Search",
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SettingsCjkFont,
|
|
8
8
|
SettingsDashboardLanguage,
|
|
9
9
|
SettingsSaveDetail,
|
|
10
|
+
SettingsAboutPageStatus,
|
|
10
11
|
} from "../settings-types.js";
|
|
11
12
|
import { MAX_SITE_NAME_LENGTH } from "../../../types.js";
|
|
12
13
|
import "../jant-settings-general.js";
|
|
@@ -76,6 +77,12 @@ const labels: SettingsLabels = {
|
|
|
76
77
|
uploadError: "Upload failed.",
|
|
77
78
|
general: "General",
|
|
78
79
|
site: "Site",
|
|
80
|
+
aboutPage: "About page",
|
|
81
|
+
aboutPagePrompt: "Want to write a fuller introduction?",
|
|
82
|
+
aboutPageConflict:
|
|
83
|
+
"/about is already used. Rename that item before creating an About page.",
|
|
84
|
+
createAboutPage: "Create About page",
|
|
85
|
+
editAboutPage: "Edit About page",
|
|
79
86
|
languageAndTime: "Language & Time",
|
|
80
87
|
home: "Home",
|
|
81
88
|
search: "Search",
|
|
@@ -167,6 +174,7 @@ function findCheckboxByLabel(
|
|
|
167
174
|
async function createElement(
|
|
168
175
|
opts: {
|
|
169
176
|
demoMode?: boolean;
|
|
177
|
+
aboutPage?: SettingsAboutPageStatus;
|
|
170
178
|
} = {},
|
|
171
179
|
): Promise<JantSettingsGeneral> {
|
|
172
180
|
const el = document.createElement(
|
|
@@ -182,6 +190,14 @@ async function createElement(
|
|
|
182
190
|
el.latestFeedUrl = "/latest/feed";
|
|
183
191
|
el.featuredFeedUrl = "/featured/feed";
|
|
184
192
|
el.archiveFeedUrl = "/archive/feed";
|
|
193
|
+
el.aboutPage =
|
|
194
|
+
opts.aboutPage ??
|
|
195
|
+
({
|
|
196
|
+
state: "missing",
|
|
197
|
+
path: "/about",
|
|
198
|
+
} satisfies SettingsAboutPageStatus);
|
|
199
|
+
el.aboutEditUrl = "/about?edit=1";
|
|
200
|
+
el.aboutCreateUrl = "/settings/general/about-page";
|
|
185
201
|
el.demoMode = opts.demoMode ?? false;
|
|
186
202
|
document.body.appendChild(el);
|
|
187
203
|
await el.updateComplete;
|
|
@@ -209,6 +225,61 @@ describe("JantSettingsGeneral", () => {
|
|
|
209
225
|
labels.home,
|
|
210
226
|
labels.search,
|
|
211
227
|
]);
|
|
228
|
+
|
|
229
|
+
const siteSection = requireElement(
|
|
230
|
+
findSectionByHeading(el, labels.site),
|
|
231
|
+
"expected site section",
|
|
232
|
+
);
|
|
233
|
+
expect(siteSection.querySelector("[data-about-page-row]")).not.toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("renders a create About form from the missing About prompt", async () => {
|
|
237
|
+
const el = await createElement();
|
|
238
|
+
const aboutRow = requireElement(
|
|
239
|
+
el.querySelector<HTMLElement>("[data-about-page-row]"),
|
|
240
|
+
"expected About page row",
|
|
241
|
+
);
|
|
242
|
+
const form = requireElement(
|
|
243
|
+
aboutRow.querySelector<HTMLFormElement>("form"),
|
|
244
|
+
"expected create About page form",
|
|
245
|
+
);
|
|
246
|
+
const button = requireElement(
|
|
247
|
+
form.querySelector<HTMLButtonElement>("button"),
|
|
248
|
+
"expected create About page button",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(aboutRow.textContent).toContain(labels.aboutPagePrompt);
|
|
252
|
+
expect(form.method).toBe("post");
|
|
253
|
+
expect(form.action).toContain("/settings/general/about-page");
|
|
254
|
+
expect(button.textContent).toContain(labels.createAboutPage);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("renders an edit link when the About page exists", async () => {
|
|
258
|
+
const el = await createElement({
|
|
259
|
+
aboutPage: {
|
|
260
|
+
state: "ready",
|
|
261
|
+
path: "/about",
|
|
262
|
+
post: {
|
|
263
|
+
id: "pst_about000000000000000000000",
|
|
264
|
+
title: "About",
|
|
265
|
+
status: "published",
|
|
266
|
+
visibility: "latest_hidden",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const aboutRow = requireElement(
|
|
272
|
+
el.querySelector<HTMLElement>("[data-about-page-row]"),
|
|
273
|
+
"expected About page row",
|
|
274
|
+
);
|
|
275
|
+
const editLink = requireElement(
|
|
276
|
+
aboutRow.querySelector<HTMLAnchorElement>("a"),
|
|
277
|
+
"expected About edit link",
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(aboutRow.textContent).toContain(labels.aboutPagePrompt);
|
|
281
|
+
expect(editLink.textContent).toContain(labels.editAboutPage);
|
|
282
|
+
expect(editLink.href).toContain("/about?edit=1");
|
|
212
283
|
});
|
|
213
284
|
|
|
214
285
|
it("renders form fields with initial values", async () => {
|
|
@@ -586,6 +657,7 @@ describe("JantSettingsGeneral", () => {
|
|
|
586
657
|
const d = detail as unknown as SettingsSaveDetail;
|
|
587
658
|
expect(d.endpoint).toBe("/settings/general/home");
|
|
588
659
|
expect(d.section).toBe("home");
|
|
660
|
+
expect(d.data).not.toHaveProperty("homeDefaultView");
|
|
589
661
|
expect(d.data.showJantBrandingOnHome).toBe(true);
|
|
590
662
|
expect(siteSaveBtn?.disabled).toBe(true);
|
|
591
663
|
});
|