@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection dialog host.
|
|
3
|
+
*
|
|
4
|
+
* One dialog element for the whole page, mounted lazily and reused. Four
|
|
5
|
+
* places open it — the collections page toolbar, a directory row's menu, a
|
|
6
|
+
* collection page's own menu, and the navigation manager — and they all go
|
|
7
|
+
* through {@link openCollectionDialog}, because creating and editing are one
|
|
8
|
+
* surface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import "./components/jant-collection-dialog.js";
|
|
12
|
+
import type { JantCollectionDialog } from "./components/jant-collection-dialog.js";
|
|
13
|
+
import type {
|
|
14
|
+
CollectionDialogLabels,
|
|
15
|
+
CollectionDialogResult,
|
|
16
|
+
} from "./components/collection-dialog-types.js";
|
|
17
|
+
|
|
18
|
+
const LABELS_ATTRIBUTE = "data-collection-dialog-labels";
|
|
19
|
+
|
|
20
|
+
let labels: CollectionDialogLabels | null = null;
|
|
21
|
+
let element: JantCollectionDialog | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The dialog's strings, rendered into the page by whichever surface can open
|
|
25
|
+
* it. Read once and cached: they are the same on every surface of a page.
|
|
26
|
+
*/
|
|
27
|
+
function readLabels(): CollectionDialogLabels | null {
|
|
28
|
+
if (labels) return labels;
|
|
29
|
+
const holder = document.querySelector<HTMLElement>(`[${LABELS_ATTRIBUTE}]`);
|
|
30
|
+
const raw = holder?.getAttribute(LABELS_ATTRIBUTE);
|
|
31
|
+
if (!raw) return null;
|
|
32
|
+
try {
|
|
33
|
+
labels = JSON.parse(raw) as CollectionDialogLabels;
|
|
34
|
+
return labels;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureElement(
|
|
41
|
+
dialogLabels: CollectionDialogLabels,
|
|
42
|
+
): JantCollectionDialog {
|
|
43
|
+
if (element?.isConnected) {
|
|
44
|
+
element.labels = dialogLabels;
|
|
45
|
+
return element;
|
|
46
|
+
}
|
|
47
|
+
element = document.createElement(
|
|
48
|
+
"jant-collection-dialog",
|
|
49
|
+
) as JantCollectionDialog;
|
|
50
|
+
element.labels = dialogLabels;
|
|
51
|
+
document.body.appendChild(element);
|
|
52
|
+
return element;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Open the collection dialog.
|
|
57
|
+
*
|
|
58
|
+
* @param options.collectionId - Edit this one; omit to create a new one
|
|
59
|
+
* @returns What happened, and the collection when one was saved
|
|
60
|
+
* @example
|
|
61
|
+
* const { changed } = await openCollectionDialog({ collectionId: "col_…" });
|
|
62
|
+
*/
|
|
63
|
+
export async function openCollectionDialog(
|
|
64
|
+
options: {
|
|
65
|
+
collectionId?: string;
|
|
66
|
+
} = {},
|
|
67
|
+
): Promise<CollectionDialogResult> {
|
|
68
|
+
const dialogLabels = readLabels();
|
|
69
|
+
if (!dialogLabels) return { changed: false };
|
|
70
|
+
|
|
71
|
+
return ensureElement(dialogLabels).open(options);
|
|
72
|
+
}
|
|
@@ -9,16 +9,14 @@ const SITE_HEADER_REQUEST_HEADER = "X-Jant-Site-Header";
|
|
|
9
9
|
const INCLUDE_SITE_HEADER_RESPONSE = "include";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Create a navigation item and refresh the rendered site header when the
|
|
13
|
+
* server returns a fragment.
|
|
14
14
|
*
|
|
15
|
-
* @param
|
|
15
|
+
* @param body - Request body for `POST /api/nav-items`
|
|
16
16
|
* @returns The created navigation item ID when present
|
|
17
|
-
* @example
|
|
18
|
-
* const navItemId = await addCollectionToNavigation("col_01abc");
|
|
19
17
|
*/
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
async function createNavItem(
|
|
19
|
+
body: Record<string, unknown>,
|
|
22
20
|
): Promise<string | undefined> {
|
|
23
21
|
const response = await fetch("/api/nav-items", {
|
|
24
22
|
method: "POST",
|
|
@@ -27,18 +25,55 @@ export async function addCollectionToNavigation(
|
|
|
27
25
|
Accept: "application/json",
|
|
28
26
|
[SITE_HEADER_REQUEST_HEADER]: INCLUDE_SITE_HEADER_RESPONSE,
|
|
29
27
|
},
|
|
30
|
-
body: JSON.stringify(
|
|
31
|
-
type: "collection",
|
|
32
|
-
collectionId,
|
|
33
|
-
placement: "header",
|
|
34
|
-
}),
|
|
28
|
+
body: JSON.stringify(body),
|
|
35
29
|
});
|
|
36
|
-
const
|
|
30
|
+
const responseBody = await readJsonObject(response);
|
|
37
31
|
|
|
38
32
|
if (!response.ok) {
|
|
39
|
-
throw new Error(
|
|
33
|
+
throw new Error(
|
|
34
|
+
getJsonString(responseBody, "error") ?? `HTTP ${response.status}`,
|
|
35
|
+
);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
applySiteHeaderHtml(getJsonString(
|
|
43
|
-
return getJsonString(
|
|
38
|
+
applySiteHeaderHtml(getJsonString(responseBody, "headerHtml"));
|
|
39
|
+
return getJsonString(responseBody, "id");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Add a Collection to the end of the primary navigation and refresh the
|
|
44
|
+
* rendered site header when the server returns a fragment.
|
|
45
|
+
*
|
|
46
|
+
* @param collectionId - TypeID of the Collection to add
|
|
47
|
+
* @returns The created navigation item ID when present
|
|
48
|
+
* @example
|
|
49
|
+
* const navItemId = await addCollectionToNavigation("col_01abc");
|
|
50
|
+
*/
|
|
51
|
+
export async function addCollectionToNavigation(
|
|
52
|
+
collectionId: string,
|
|
53
|
+
): Promise<string | undefined> {
|
|
54
|
+
return createNavItem({
|
|
55
|
+
type: "collection",
|
|
56
|
+
collectionId,
|
|
57
|
+
placement: "header",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add a smart collection to the end of the primary navigation. Placement
|
|
63
|
+
* matches {@link addCollectionToNavigation} — the two kinds are one thing to
|
|
64
|
+
* the reader, so the same action puts them in the same place.
|
|
65
|
+
*
|
|
66
|
+
* @param smartCollectionId - TypeID of the smart collection to add
|
|
67
|
+
* @returns The created navigation item ID when present
|
|
68
|
+
* @example
|
|
69
|
+
* const navItemId = await addSmartCollectionToNavigation("smc_01abc");
|
|
70
|
+
*/
|
|
71
|
+
export async function addSmartCollectionToNavigation(
|
|
72
|
+
smartCollectionId: string,
|
|
73
|
+
): Promise<string | undefined> {
|
|
74
|
+
return createNavItem({
|
|
75
|
+
type: "smart_collection",
|
|
76
|
+
smartCollectionId,
|
|
77
|
+
placement: "header",
|
|
78
|
+
});
|
|
44
79
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCollectionsDirectoryPath } from "../lib/collection-paths.js";
|
|
2
2
|
import { NAVIGATION_SETTINGS_PATH } from "../lib/settings-paths.js";
|
|
3
|
+
import { openCollectionDialog } from "./collection-dialog-host.js";
|
|
3
4
|
import { addCollectionToNavigation } from "./collection-navigation.js";
|
|
4
5
|
import { showConfirmDialog } from "./confirm.js";
|
|
5
6
|
import { showToast, showToastWithAction } from "./toast.js";
|
|
@@ -95,6 +96,27 @@ document
|
|
|
95
96
|
}
|
|
96
97
|
};
|
|
97
98
|
|
|
99
|
+
const handleEdit = async () => {
|
|
100
|
+
closeMenu(false);
|
|
101
|
+
const { changed, collection } = await openCollectionDialog({
|
|
102
|
+
collectionId,
|
|
103
|
+
});
|
|
104
|
+
if (!changed) return;
|
|
105
|
+
|
|
106
|
+
// The address can have moved, and the old one stops resolving the moment
|
|
107
|
+
// it does — so the page follows it rather than reloading into a 404.
|
|
108
|
+
// Only the last segment changes: whatever site or language prefix this
|
|
109
|
+
// reader arrived under is theirs to keep.
|
|
110
|
+
const nextPath = collection?.slug
|
|
111
|
+
? window.location.pathname.replace(/[^/]*$/, collection.slug)
|
|
112
|
+
: null;
|
|
113
|
+
if (nextPath && nextPath !== window.location.pathname) {
|
|
114
|
+
window.location.href = `${nextPath}${window.location.search}`;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
window.location.reload();
|
|
118
|
+
};
|
|
119
|
+
|
|
98
120
|
const handleDelete = async () => {
|
|
99
121
|
closeMenu(false);
|
|
100
122
|
|
|
@@ -197,6 +219,10 @@ document
|
|
|
197
219
|
if (!actionEl || !root.contains(actionEl)) return;
|
|
198
220
|
|
|
199
221
|
const action = actionEl.dataset.collectionPageAction;
|
|
222
|
+
if (action === "edit") {
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
void handleEdit();
|
|
225
|
+
}
|
|
200
226
|
if (action === "delete") {
|
|
201
227
|
event.preventDefault();
|
|
202
228
|
void handleDelete();
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
vi.mock("../../toast.js", () => ({
|
|
6
|
+
showToast: vi.fn(),
|
|
7
|
+
showToastWithAction: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("../../lazy-slugify.js", () => ({
|
|
11
|
+
slugify: (text: string) =>
|
|
12
|
+
Promise.resolve(
|
|
13
|
+
text
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/[^\w\s-]/g, "")
|
|
17
|
+
.replace(/[\s_-]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, ""),
|
|
19
|
+
),
|
|
20
|
+
preloadSlug: () => {},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// happy-dom has no contenteditable, so the description editor is stood in for.
|
|
24
|
+
let editorContent: string | undefined;
|
|
25
|
+
vi.mock("../../tiptap/create-editor.js", () => ({
|
|
26
|
+
createSettingsEditor: (opts: { element: HTMLElement; content?: string }) => {
|
|
27
|
+
editorContent = opts.content;
|
|
28
|
+
opts.element.innerHTML =
|
|
29
|
+
'<div class="ProseMirror" contenteditable="true"></div>';
|
|
30
|
+
return {
|
|
31
|
+
getJSON: () => ({ type: "doc", content: [] }),
|
|
32
|
+
destroy: () => {},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
jsonToMarkdown: () => "",
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
import "../jant-collection-dialog.js";
|
|
39
|
+
import type { JantCollectionDialog } from "../jant-collection-dialog.js";
|
|
40
|
+
import type { CollectionDialogLabels } from "../collection-dialog-types.js";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The dialog is now the only place a collection is written, so what it has to
|
|
44
|
+
* hold is what the editor page used to: an address derived from the title and
|
|
45
|
+
* checked against the server, a warning before an existing link moves, and a
|
|
46
|
+
* save that reports where the collection ended up.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
const labels: CollectionDialogLabels = {
|
|
50
|
+
createHeading: "New Collection",
|
|
51
|
+
editHeading: "Edit Collection",
|
|
52
|
+
title: "Title",
|
|
53
|
+
titlePlaceholder: "My Collection",
|
|
54
|
+
link: "Collection link",
|
|
55
|
+
linkHelp: "This is the last part of the collection link.",
|
|
56
|
+
editLink: "Edit link",
|
|
57
|
+
resetLink: "Reset link",
|
|
58
|
+
linkTaken: "This link is taken. Choose another.",
|
|
59
|
+
linkInvalid: "Use lowercase letters, numbers, and hyphens only.",
|
|
60
|
+
linkReserved: "This link is reserved. Choose something else.",
|
|
61
|
+
linkTooLong: "Keep this link under 200 characters.",
|
|
62
|
+
linkMovesWarning: "Changing the link breaks the old one immediately.",
|
|
63
|
+
description: "Description (optional)",
|
|
64
|
+
descriptionPlaceholder: "What's this collection about?",
|
|
65
|
+
orderBy: "Order by",
|
|
66
|
+
sortOptions: {
|
|
67
|
+
newest: "Newest first",
|
|
68
|
+
oldest: "Oldest first",
|
|
69
|
+
rating_desc: "Highest rated",
|
|
70
|
+
},
|
|
71
|
+
cancel: "Cancel",
|
|
72
|
+
save: "Save",
|
|
73
|
+
saved: "Collection saved.",
|
|
74
|
+
saveFailed: "Couldn't save. Try again in a moment.",
|
|
75
|
+
loadFailed: "Could not open this collection. Try again.",
|
|
76
|
+
titleAndLinkRequired: "A collection needs a title and a link.",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Responses the dialog's fetches get, keyed by the path they hit. */
|
|
80
|
+
let responses: Record<string, { ok?: boolean; body: unknown }>;
|
|
81
|
+
let requests: Array<{ url: string; method: string; body: unknown }>;
|
|
82
|
+
|
|
83
|
+
function mountDialog(): JantCollectionDialog {
|
|
84
|
+
const element = document.createElement(
|
|
85
|
+
"jant-collection-dialog",
|
|
86
|
+
) as JantCollectionDialog;
|
|
87
|
+
element.labels = labels;
|
|
88
|
+
document.body.appendChild(element);
|
|
89
|
+
return element;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Let the debounced address check and the slugify round trip settle. */
|
|
93
|
+
async function settle(element: JantCollectionDialog) {
|
|
94
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
95
|
+
await element.updateComplete;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findButton(
|
|
99
|
+
element: JantCollectionDialog,
|
|
100
|
+
text: string,
|
|
101
|
+
): HTMLButtonElement | undefined {
|
|
102
|
+
return [...element.querySelectorAll("button")].find(
|
|
103
|
+
(button) => button.textContent?.trim() === text,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function typeTitle(element: JantCollectionDialog, value: string) {
|
|
108
|
+
const input = element.querySelector<HTMLInputElement>("[data-field='title']");
|
|
109
|
+
if (!input) throw new Error("Expected the title field");
|
|
110
|
+
input.value = value;
|
|
111
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
112
|
+
await settle(element);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
document.body.innerHTML = "";
|
|
117
|
+
vi.useFakeTimers();
|
|
118
|
+
editorContent = undefined;
|
|
119
|
+
|
|
120
|
+
responses = {
|
|
121
|
+
"/api/collections/slug": { body: { slug: "reading", available: true } },
|
|
122
|
+
"/api/collections": { body: { id: "col_new", slug: "reading" } },
|
|
123
|
+
"/api/collections/col_books": {
|
|
124
|
+
body: {
|
|
125
|
+
id: "col_books",
|
|
126
|
+
slug: "books",
|
|
127
|
+
title: "Books",
|
|
128
|
+
description: "Notes from books",
|
|
129
|
+
sortOrder: "oldest",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
requests = [];
|
|
134
|
+
|
|
135
|
+
vi.stubGlobal(
|
|
136
|
+
"fetch",
|
|
137
|
+
vi.fn(async (url: string, init?: globalThis.RequestInit) => {
|
|
138
|
+
const path = url.split("?")[0] ?? url;
|
|
139
|
+
requests.push({
|
|
140
|
+
url,
|
|
141
|
+
method: init?.method ?? "GET",
|
|
142
|
+
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
143
|
+
});
|
|
144
|
+
const response = responses[path] ?? { body: {} };
|
|
145
|
+
return {
|
|
146
|
+
ok: response.ok ?? true,
|
|
147
|
+
status: response.ok === false ? 400 : 200,
|
|
148
|
+
json: async () => response.body,
|
|
149
|
+
} as Response;
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// happy-dom's <dialog> has no modal implementation.
|
|
154
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
155
|
+
this.open = true;
|
|
156
|
+
};
|
|
157
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
158
|
+
this.open = false;
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("JantCollectionDialog", () => {
|
|
163
|
+
it("opens on the create heading with nothing filled in", async () => {
|
|
164
|
+
const element = mountDialog();
|
|
165
|
+
void element.open({});
|
|
166
|
+
await settle(element);
|
|
167
|
+
|
|
168
|
+
expect(element.textContent).toContain("New Collection");
|
|
169
|
+
expect(
|
|
170
|
+
element.querySelector<HTMLInputElement>("[data-field='title']")?.value,
|
|
171
|
+
).toBe("");
|
|
172
|
+
// The link only appears once there is one to show.
|
|
173
|
+
expect(element.querySelector(".collection-quick-link-box")).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("derives the address from the title and asks whether it is free", async () => {
|
|
177
|
+
const element = mountDialog();
|
|
178
|
+
void element.open({});
|
|
179
|
+
await settle(element);
|
|
180
|
+
await typeTitle(element, "Reading");
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
element.querySelector<HTMLElement>(".collection-quick-link-preview")
|
|
184
|
+
?.textContent,
|
|
185
|
+
).toContain("/reading");
|
|
186
|
+
expect(
|
|
187
|
+
requests.some(
|
|
188
|
+
(request) =>
|
|
189
|
+
request.url.includes("/api/collections/slug") &&
|
|
190
|
+
request.url.includes("slug=reading"),
|
|
191
|
+
),
|
|
192
|
+
).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("refuses to save onto an address something else already holds", async () => {
|
|
196
|
+
responses["/api/collections/slug"] = {
|
|
197
|
+
body: { slug: "reading", available: false },
|
|
198
|
+
};
|
|
199
|
+
const element = mountDialog();
|
|
200
|
+
void element.open({});
|
|
201
|
+
await settle(element);
|
|
202
|
+
await typeTitle(element, "Reading");
|
|
203
|
+
|
|
204
|
+
expect(element.textContent).toContain(
|
|
205
|
+
"This link is taken. Choose another.",
|
|
206
|
+
);
|
|
207
|
+
expect(findButton(element, "Save")?.disabled).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("loads an existing collection, and warns before its link moves", async () => {
|
|
211
|
+
const element = mountDialog();
|
|
212
|
+
void element.open({ collectionId: "col_books" });
|
|
213
|
+
await settle(element);
|
|
214
|
+
|
|
215
|
+
expect(element.textContent).toContain("Edit Collection");
|
|
216
|
+
expect(
|
|
217
|
+
element.querySelector<HTMLInputElement>("[data-field='title']")?.value,
|
|
218
|
+
).toBe("Books");
|
|
219
|
+
const sortSelect = element.querySelector(
|
|
220
|
+
"[data-field='sort']",
|
|
221
|
+
) as HTMLSelectElement | null;
|
|
222
|
+
expect(sortSelect?.value).toBe("oldest");
|
|
223
|
+
expect(editorContent).toBe("Notes from books");
|
|
224
|
+
expect(element.textContent).not.toContain(
|
|
225
|
+
"Changing the link breaks the old one immediately.",
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
findButton(element, "Edit link")?.click();
|
|
229
|
+
await element.updateComplete;
|
|
230
|
+
const slugInput = element.querySelector<HTMLInputElement>(
|
|
231
|
+
"[data-field='slug']",
|
|
232
|
+
);
|
|
233
|
+
if (!slugInput) throw new Error("Expected the link field");
|
|
234
|
+
slugInput.value = "reading";
|
|
235
|
+
slugInput.dispatchEvent(new Event("input", { bubbles: true }));
|
|
236
|
+
await settle(element);
|
|
237
|
+
|
|
238
|
+
expect(element.textContent).toContain(
|
|
239
|
+
"Changing the link breaks the old one immediately.",
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("creates a collection and reports where it landed", async () => {
|
|
244
|
+
const element = mountDialog();
|
|
245
|
+
const result = element.open({});
|
|
246
|
+
await settle(element);
|
|
247
|
+
await typeTitle(element, "Reading");
|
|
248
|
+
|
|
249
|
+
findButton(element, "Save")?.click();
|
|
250
|
+
await settle(element);
|
|
251
|
+
|
|
252
|
+
const save = requests.find((request) => request.method === "POST");
|
|
253
|
+
expect(save?.url).toContain("/api/collections");
|
|
254
|
+
expect(save?.body).toEqual({
|
|
255
|
+
slug: "reading",
|
|
256
|
+
title: "Reading",
|
|
257
|
+
sortOrder: "newest",
|
|
258
|
+
});
|
|
259
|
+
await expect(result).resolves.toEqual({
|
|
260
|
+
changed: true,
|
|
261
|
+
collection: { id: "col_new", slug: "reading", title: "Reading" },
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("saves an edit with PUT, and clears a description that was emptied", async () => {
|
|
266
|
+
responses["/api/collections/col_books"] = {
|
|
267
|
+
body: {
|
|
268
|
+
id: "col_books",
|
|
269
|
+
slug: "books",
|
|
270
|
+
title: "Books",
|
|
271
|
+
description: "Notes from books",
|
|
272
|
+
sortOrder: "newest",
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
const element = mountDialog();
|
|
276
|
+
void element.open({ collectionId: "col_books" });
|
|
277
|
+
await settle(element);
|
|
278
|
+
|
|
279
|
+
findButton(element, "Save")?.click();
|
|
280
|
+
await settle(element);
|
|
281
|
+
|
|
282
|
+
const save = requests.find((request) => request.method === "PUT");
|
|
283
|
+
expect(save?.url).toContain("/api/collections/col_books");
|
|
284
|
+
// The stubbed editor round-trips to an empty document, so the edit is
|
|
285
|
+
// clearing the description — which only an explicit null can do.
|
|
286
|
+
expect(save?.body).toEqual({
|
|
287
|
+
slug: "books",
|
|
288
|
+
title: "Books",
|
|
289
|
+
sortOrder: "newest",
|
|
290
|
+
description: null,
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("keeps a failed save open, with the reason the server gave", async () => {
|
|
295
|
+
responses["/api/collections"] = {
|
|
296
|
+
ok: false,
|
|
297
|
+
body: { error: "That link is reserved." },
|
|
298
|
+
};
|
|
299
|
+
const element = mountDialog();
|
|
300
|
+
void element.open({});
|
|
301
|
+
await settle(element);
|
|
302
|
+
await typeTitle(element, "Reading");
|
|
303
|
+
|
|
304
|
+
findButton(element, "Save")?.click();
|
|
305
|
+
await settle(element);
|
|
306
|
+
|
|
307
|
+
expect(element.textContent).toContain("That link is reserved.");
|
|
308
|
+
expect(
|
|
309
|
+
element.querySelector<HTMLDialogElement>(".collection-dialog")?.open,
|
|
310
|
+
).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("closes on Escape without saving", async () => {
|
|
314
|
+
const element = mountDialog();
|
|
315
|
+
const result = element.open({});
|
|
316
|
+
await settle(element);
|
|
317
|
+
await typeTitle(element, "Reading");
|
|
318
|
+
|
|
319
|
+
element
|
|
320
|
+
.querySelector<HTMLInputElement>("[data-field='title']")
|
|
321
|
+
?.dispatchEvent(
|
|
322
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
323
|
+
);
|
|
324
|
+
await element.updateComplete;
|
|
325
|
+
|
|
326
|
+
await expect(result).resolves.toEqual({ changed: false });
|
|
327
|
+
expect(requests.some((request) => request.method === "POST")).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
});
|