@jant/core 0.6.14 → 0.6.15
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 +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* with fractional indexing for efficient reordering.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { and, eq, asc, sql, inArray } from "drizzle-orm";
|
|
8
|
+
import { and, eq, asc, desc, isNull, sql, inArray } from "drizzle-orm";
|
|
9
9
|
import { generateKeyBetween } from "fractional-indexing";
|
|
10
10
|
import type { Database } from "../db/index.js";
|
|
11
11
|
import {
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
stripSitePathPrefix,
|
|
22
22
|
toSameSitePath,
|
|
23
23
|
} from "../lib/url.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
COLLECTION_FRESHNESS_WINDOW_SECONDS,
|
|
26
|
+
DEFAULT_NAVIGATION_PROFILE,
|
|
27
|
+
} from "../types.js";
|
|
25
28
|
import type {
|
|
26
29
|
NavItem,
|
|
27
30
|
NavItemType,
|
|
@@ -30,6 +33,7 @@ import type {
|
|
|
30
33
|
UpdateNavItem,
|
|
31
34
|
SystemNavKey,
|
|
32
35
|
SuggestedNavLink,
|
|
36
|
+
NavigationPageCandidate,
|
|
33
37
|
} from "../types.js";
|
|
34
38
|
import { SYSTEM_NAV_KEYS } from "../types.js";
|
|
35
39
|
|
|
@@ -49,11 +53,18 @@ import { isUniqueConstraintError } from "../db/dialect.js";
|
|
|
49
53
|
|
|
50
54
|
export interface NavItemService {
|
|
51
55
|
list(): Promise<NavItem[]>;
|
|
56
|
+
listPageCandidates(options?: {
|
|
57
|
+
query?: string;
|
|
58
|
+
limit?: number;
|
|
59
|
+
}): Promise<NavigationPageCandidate[]>;
|
|
52
60
|
getById(id: string): Promise<NavItem | null>;
|
|
53
61
|
create(data: CreateNavItem): Promise<NavItem>;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Add missing items from the current default profile without changing
|
|
64
|
+
* existing navigation state. Call only during provisioning or incomplete
|
|
65
|
+
* onboarding recovery.
|
|
66
|
+
*/
|
|
67
|
+
materializeDefaultNavigation(): Promise<NavItem[]>;
|
|
57
68
|
update(id: string, data: UpdateNavItem): Promise<NavItem | null>;
|
|
58
69
|
delete(id: string): Promise<boolean>;
|
|
59
70
|
move(
|
|
@@ -72,18 +83,9 @@ export function createNavItemService(
|
|
|
72
83
|
siteId: string,
|
|
73
84
|
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
74
85
|
): NavItemService {
|
|
75
|
-
const { navItems,
|
|
86
|
+
const { navItems, threadCollections, posts, pathRegistry, collections } =
|
|
76
87
|
databaseSchema;
|
|
77
88
|
|
|
78
|
-
const defaultSystemOrder = [
|
|
79
|
-
"latest",
|
|
80
|
-
"featured",
|
|
81
|
-
"collections",
|
|
82
|
-
"archive",
|
|
83
|
-
"rss",
|
|
84
|
-
"settings",
|
|
85
|
-
] as const satisfies readonly SystemNavKey[];
|
|
86
|
-
|
|
87
89
|
function toNavItem(row: typeof navItems.$inferSelect): NavItem {
|
|
88
90
|
return {
|
|
89
91
|
id: row.id,
|
|
@@ -91,6 +93,7 @@ export function createNavItemService(
|
|
|
91
93
|
type: row.type as NavItemType,
|
|
92
94
|
systemKey: (row.systemKey as SystemNavKey | null) ?? undefined,
|
|
93
95
|
collectionId: row.collectionId ?? undefined,
|
|
96
|
+
postId: row.postId ?? undefined,
|
|
94
97
|
label: row.label,
|
|
95
98
|
url: row.url,
|
|
96
99
|
placement: (row.placement ?? "header") as NavItemPlacement,
|
|
@@ -100,7 +103,7 @@ export function createNavItemService(
|
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
function normalizeCreateData(data: CreateNavItem) {
|
|
106
|
+
async function normalizeCreateData(data: CreateNavItem) {
|
|
104
107
|
if (data.type === "system") {
|
|
105
108
|
const config = SYSTEM_NAV_KEYS[data.systemKey];
|
|
106
109
|
if (!config) {
|
|
@@ -111,6 +114,7 @@ export function createNavItemService(
|
|
|
111
114
|
type: data.type,
|
|
112
115
|
systemKey: data.systemKey,
|
|
113
116
|
collectionId: null,
|
|
117
|
+
postId: null,
|
|
114
118
|
label: "",
|
|
115
119
|
url: config.url,
|
|
116
120
|
placement: data.placement ?? config.defaultPlacement,
|
|
@@ -123,6 +127,7 @@ export function createNavItemService(
|
|
|
123
127
|
type: data.type,
|
|
124
128
|
systemKey: null,
|
|
125
129
|
collectionId: data.collectionId,
|
|
130
|
+
postId: null,
|
|
126
131
|
label: data.label,
|
|
127
132
|
url: data.url,
|
|
128
133
|
placement: data.placement ?? "header",
|
|
@@ -130,10 +135,57 @@ export function createNavItemService(
|
|
|
130
135
|
};
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
if (data.type === "page") {
|
|
139
|
+
const rows = await db
|
|
140
|
+
.select({
|
|
141
|
+
title: posts.title,
|
|
142
|
+
slug: pathRegistry.path,
|
|
143
|
+
})
|
|
144
|
+
.from(posts)
|
|
145
|
+
.innerJoin(
|
|
146
|
+
pathRegistry,
|
|
147
|
+
and(
|
|
148
|
+
eq(pathRegistry.siteId, siteId),
|
|
149
|
+
eq(pathRegistry.postId, posts.id),
|
|
150
|
+
eq(pathRegistry.kind, "slug"),
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
.where(
|
|
154
|
+
and(
|
|
155
|
+
eq(posts.siteId, siteId),
|
|
156
|
+
eq(posts.id, data.postId),
|
|
157
|
+
eq(posts.format, "note"),
|
|
158
|
+
eq(posts.status, "published"),
|
|
159
|
+
sql`${posts.visibility} != 'private'`,
|
|
160
|
+
isNull(posts.replyToId),
|
|
161
|
+
sql`trim(${posts.title}) != ''`,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
.limit(1);
|
|
165
|
+
const page = rows[0];
|
|
166
|
+
if (!page?.title) {
|
|
167
|
+
throw new ValidationError(
|
|
168
|
+
"Page must be a published, non-private titled note",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
type: data.type,
|
|
174
|
+
systemKey: null,
|
|
175
|
+
collectionId: null,
|
|
176
|
+
postId: data.postId,
|
|
177
|
+
label: (data.label?.trim() || page.title.trim()).slice(0, 100),
|
|
178
|
+
url: withLeadingSlash(page.slug),
|
|
179
|
+
placement: data.placement ?? "header",
|
|
180
|
+
position: data.position,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
133
184
|
return {
|
|
134
185
|
type: data.type,
|
|
135
186
|
systemKey: null,
|
|
136
187
|
collectionId: null,
|
|
188
|
+
postId: null,
|
|
137
189
|
label: data.label,
|
|
138
190
|
url: data.url,
|
|
139
191
|
placement: data.placement ?? "header",
|
|
@@ -256,6 +308,63 @@ export function createNavItemService(
|
|
|
256
308
|
return rows.map(toNavItem);
|
|
257
309
|
},
|
|
258
310
|
|
|
311
|
+
async listPageCandidates(options = {}) {
|
|
312
|
+
const query = options.query?.trim() ?? "";
|
|
313
|
+
const limit = Math.min(Math.max(Math.trunc(options.limit ?? 20), 1), 50);
|
|
314
|
+
const escapedQuery = query.replace(/[\\%_]/g, "\\$&");
|
|
315
|
+
const conditions = [
|
|
316
|
+
eq(posts.siteId, siteId),
|
|
317
|
+
eq(posts.format, "note"),
|
|
318
|
+
eq(posts.status, "published"),
|
|
319
|
+
sql`${posts.visibility} != 'private'`,
|
|
320
|
+
isNull(posts.replyToId),
|
|
321
|
+
sql`trim(${posts.title}) != ''`,
|
|
322
|
+
isNull(navItems.id),
|
|
323
|
+
];
|
|
324
|
+
if (query) {
|
|
325
|
+
conditions.push(
|
|
326
|
+
sql`lower(${posts.title}) LIKE lower(${`%${escapedQuery}%`}) ESCAPE '\\'`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const rows = await db
|
|
331
|
+
.select({
|
|
332
|
+
id: posts.id,
|
|
333
|
+
title: posts.title,
|
|
334
|
+
slug: pathRegistry.path,
|
|
335
|
+
updatedAt: posts.updatedAt,
|
|
336
|
+
})
|
|
337
|
+
.from(posts)
|
|
338
|
+
.innerJoin(
|
|
339
|
+
pathRegistry,
|
|
340
|
+
and(
|
|
341
|
+
eq(pathRegistry.siteId, siteId),
|
|
342
|
+
eq(pathRegistry.postId, posts.id),
|
|
343
|
+
eq(pathRegistry.kind, "slug"),
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
.leftJoin(
|
|
347
|
+
navItems,
|
|
348
|
+
and(eq(navItems.siteId, siteId), eq(navItems.postId, posts.id)),
|
|
349
|
+
)
|
|
350
|
+
.where(and(...conditions))
|
|
351
|
+
.orderBy(desc(posts.updatedAt), desc(posts.id))
|
|
352
|
+
.limit(limit);
|
|
353
|
+
|
|
354
|
+
return rows.flatMap((row) =>
|
|
355
|
+
row.title
|
|
356
|
+
? [
|
|
357
|
+
{
|
|
358
|
+
id: row.id,
|
|
359
|
+
title: row.title,
|
|
360
|
+
slug: row.slug,
|
|
361
|
+
updatedAt: row.updatedAt,
|
|
362
|
+
},
|
|
363
|
+
]
|
|
364
|
+
: [],
|
|
365
|
+
);
|
|
366
|
+
},
|
|
367
|
+
|
|
259
368
|
async getById(id) {
|
|
260
369
|
const result = await db
|
|
261
370
|
.select()
|
|
@@ -268,7 +377,7 @@ export function createNavItemService(
|
|
|
268
377
|
async create(data) {
|
|
269
378
|
const id = createEntityId("navItem");
|
|
270
379
|
const timestamp = now();
|
|
271
|
-
const normalized = normalizeCreateData(data);
|
|
380
|
+
const normalized = await normalizeCreateData(data);
|
|
272
381
|
|
|
273
382
|
if (normalized.systemKey) {
|
|
274
383
|
const existingSystemItem = await db
|
|
@@ -304,6 +413,23 @@ export function createNavItemService(
|
|
|
304
413
|
}
|
|
305
414
|
}
|
|
306
415
|
|
|
416
|
+
if (normalized.postId) {
|
|
417
|
+
const existingPageItem = await db
|
|
418
|
+
.select({ id: navItems.id })
|
|
419
|
+
.from(navItems)
|
|
420
|
+
.where(
|
|
421
|
+
and(
|
|
422
|
+
eq(navItems.siteId, siteId),
|
|
423
|
+
eq(navItems.postId, normalized.postId),
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
.limit(1);
|
|
427
|
+
|
|
428
|
+
if (existingPageItem[0]) {
|
|
429
|
+
throw new ValidationError("Page already added to navigation");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
307
433
|
if (normalized.position !== undefined) {
|
|
308
434
|
const result = await db
|
|
309
435
|
.insert(navItems)
|
|
@@ -313,6 +439,7 @@ export function createNavItemService(
|
|
|
313
439
|
type: normalized.type,
|
|
314
440
|
systemKey: normalized.systemKey,
|
|
315
441
|
collectionId: normalized.collectionId,
|
|
442
|
+
postId: normalized.postId,
|
|
316
443
|
label: normalized.label,
|
|
317
444
|
url: normalized.url,
|
|
318
445
|
placement: normalized.placement,
|
|
@@ -336,6 +463,7 @@ export function createNavItemService(
|
|
|
336
463
|
type: normalized.type,
|
|
337
464
|
systemKey: normalized.systemKey,
|
|
338
465
|
collectionId: normalized.collectionId,
|
|
466
|
+
postId: normalized.postId,
|
|
339
467
|
label: normalized.label,
|
|
340
468
|
url: normalized.url,
|
|
341
469
|
placement: normalized.placement,
|
|
@@ -360,7 +488,7 @@ export function createNavItemService(
|
|
|
360
488
|
throw new Error("Failed to assign a unique nav item position");
|
|
361
489
|
},
|
|
362
490
|
|
|
363
|
-
async
|
|
491
|
+
async materializeDefaultNavigation() {
|
|
364
492
|
const existingRows = await db
|
|
365
493
|
.select({ systemKey: navItems.systemKey })
|
|
366
494
|
.from(navItems)
|
|
@@ -377,7 +505,7 @@ export function createNavItemService(
|
|
|
377
505
|
);
|
|
378
506
|
|
|
379
507
|
const created: NavItem[] = [];
|
|
380
|
-
for (const systemKey of systemKeys) {
|
|
508
|
+
for (const systemKey of DEFAULT_NAVIGATION_PROFILE.systemKeys) {
|
|
381
509
|
if (existing.has(systemKey)) continue;
|
|
382
510
|
try {
|
|
383
511
|
created.push(
|
|
@@ -408,12 +536,18 @@ export function createNavItemService(
|
|
|
408
536
|
.limit(1);
|
|
409
537
|
if (!existing[0]) return null;
|
|
410
538
|
|
|
411
|
-
if (
|
|
539
|
+
if (
|
|
540
|
+
existing[0].type === "system" ||
|
|
541
|
+
existing[0].type === "collection" ||
|
|
542
|
+
existing[0].type === "page"
|
|
543
|
+
) {
|
|
412
544
|
if (data.url !== undefined) {
|
|
413
545
|
throw new ValidationError(
|
|
414
546
|
existing[0].type === "system"
|
|
415
547
|
? "Built-in navigation URLs are managed automatically"
|
|
416
|
-
:
|
|
548
|
+
: existing[0].type === "collection"
|
|
549
|
+
? "Collection navigation URLs are managed automatically"
|
|
550
|
+
: "Page navigation URLs are managed automatically",
|
|
417
551
|
);
|
|
418
552
|
}
|
|
419
553
|
}
|
|
@@ -528,9 +662,12 @@ export function createNavItemService(
|
|
|
528
662
|
if (record.postId) {
|
|
529
663
|
const postRows = await db
|
|
530
664
|
.select({
|
|
665
|
+
id: posts.id,
|
|
666
|
+
format: posts.format,
|
|
531
667
|
title: posts.title,
|
|
532
668
|
status: posts.status,
|
|
533
669
|
visibility: posts.visibility,
|
|
670
|
+
replyToId: posts.replyToId,
|
|
534
671
|
})
|
|
535
672
|
.from(posts)
|
|
536
673
|
.where(and(eq(posts.siteId, siteId), eq(posts.id, record.postId)))
|
|
@@ -538,8 +675,11 @@ export function createNavItemService(
|
|
|
538
675
|
const post = postRows[0];
|
|
539
676
|
if (
|
|
540
677
|
!post ||
|
|
678
|
+
post.format !== "note" ||
|
|
541
679
|
post.status !== "published" ||
|
|
542
|
-
post.visibility === "private"
|
|
680
|
+
post.visibility === "private" ||
|
|
681
|
+
post.replyToId !== null ||
|
|
682
|
+
!post.title?.trim()
|
|
543
683
|
) {
|
|
544
684
|
continue;
|
|
545
685
|
}
|
|
@@ -549,7 +689,8 @@ export function createNavItemService(
|
|
|
549
689
|
label: normalizeSuggestedLabel(post.title, candidate.label),
|
|
550
690
|
url: path,
|
|
551
691
|
targetType: "page",
|
|
552
|
-
navItemType: "
|
|
692
|
+
navItemType: "page",
|
|
693
|
+
postId: post.id,
|
|
553
694
|
});
|
|
554
695
|
continue;
|
|
555
696
|
}
|
|
@@ -603,47 +744,45 @@ export function createNavItemService(
|
|
|
603
744
|
if (collectionIds.length === 0) return new Map<string, number>();
|
|
604
745
|
|
|
605
746
|
const threshold = now() - COLLECTION_FRESHNESS_WINDOW_SECONDS;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
747
|
+
const threadActivityAt = sql<number>`CASE
|
|
748
|
+
WHEN ${posts.updatedAt} > ${posts.createdAt}
|
|
749
|
+
AND ${posts.updatedAt} > COALESCE(${posts.lastActivityAt}, -1)
|
|
750
|
+
THEN ${posts.updatedAt}
|
|
751
|
+
ELSE COALESCE(${posts.lastActivityAt}, ${posts.updatedAt})
|
|
752
|
+
END`;
|
|
753
|
+
|
|
754
|
+
// Find collections with recent Thread activity and its latest timestamp.
|
|
755
|
+
// `lastActivityAt` is maintained on the root when a non-quiet reply is
|
|
756
|
+
// published. A later `updatedAt` keeps actual root edits visible without
|
|
757
|
+
// treating a freshly imported historical root as newly active.
|
|
611
758
|
const rows = await db
|
|
612
759
|
.select({
|
|
613
|
-
collectionId:
|
|
760
|
+
collectionId: threadCollections.collectionId,
|
|
614
761
|
latestAt:
|
|
615
|
-
sql<number>`MAX(CASE WHEN ${
|
|
762
|
+
sql<number>`MAX(CASE WHEN ${threadCollections.createdAt} > ${threadActivityAt} THEN ${threadCollections.createdAt} ELSE ${threadActivityAt} END)`.as(
|
|
616
763
|
"latest_at",
|
|
617
764
|
),
|
|
618
765
|
})
|
|
619
|
-
.from(
|
|
766
|
+
.from(threadCollections)
|
|
620
767
|
.innerJoin(
|
|
621
768
|
posts,
|
|
622
769
|
and(
|
|
623
|
-
eq(posts.id,
|
|
624
|
-
eq(posts.siteId,
|
|
770
|
+
eq(posts.id, threadCollections.threadId),
|
|
771
|
+
eq(posts.siteId, threadCollections.siteId),
|
|
625
772
|
),
|
|
626
773
|
)
|
|
627
774
|
.where(
|
|
628
775
|
and(
|
|
629
|
-
eq(
|
|
630
|
-
inArray(
|
|
776
|
+
eq(threadCollections.siteId, siteId),
|
|
777
|
+
inArray(threadCollections.collectionId, collectionIds),
|
|
631
778
|
sql`(
|
|
632
|
-
${
|
|
633
|
-
OR (${
|
|
779
|
+
${threadCollections.createdAt} > ${threshold}
|
|
780
|
+
OR (${threadActivityAt} > ${threshold}
|
|
634
781
|
AND ${posts.status} = 'published')
|
|
635
|
-
OR EXISTS (
|
|
636
|
-
SELECT 1 FROM ${posts} reply
|
|
637
|
-
WHERE reply.site_id = ${postCollections.siteId}
|
|
638
|
-
AND reply.thread_id = ${postCollections.postId}
|
|
639
|
-
AND reply.reply_to_id IS NOT NULL
|
|
640
|
-
AND reply.status = 'published'
|
|
641
|
-
AND reply.created_at > ${threshold}
|
|
642
|
-
)
|
|
643
782
|
)`,
|
|
644
783
|
),
|
|
645
784
|
)
|
|
646
|
-
.groupBy(
|
|
785
|
+
.groupBy(threadCollections.collectionId);
|
|
647
786
|
|
|
648
787
|
return new Map(rows.map((r) => [r.collectionId, r.latestAt]));
|
|
649
788
|
},
|