includio-cms 0.24.1 → 0.26.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/API.md +57 -4
- package/CHANGELOG.md +53 -0
- package/DOCS.md +1 -1
- package/README.md +2 -0
- package/ROADMAP.md +6 -0
- package/dist/admin/client/account/lang.d.ts +1 -0
- package/dist/admin/client/account/lang.js +4 -2
- package/dist/admin/client/account/profile-section.svelte +2 -2
- package/dist/admin/client/account/security-section.svelte +27 -4
- package/dist/admin/client/account/sessions-section.svelte +1 -1
- package/dist/admin/client/admin/admin-after-login-layout-content.svelte +1 -1
- package/dist/admin/client/admin/dashboard-page.svelte +34 -10
- package/dist/admin/client/collection/bulk-actions-bar.svelte +86 -44
- package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +3 -1
- package/dist/admin/client/collection/collection-entries.svelte +52 -36
- package/dist/admin/client/collection/collection-entries.svelte.d.ts +3 -0
- package/dist/admin/client/collection/collection.svelte +28 -14
- package/dist/admin/client/collection/collection.svelte.d.ts +3 -0
- package/dist/admin/client/collection/data-table.svelte +279 -130
- package/dist/admin/client/collection/data-table.svelte.d.ts +11 -0
- package/dist/admin/client/collection/date-cell.svelte +4 -4
- package/dist/admin/client/collection/row-actions.svelte +2 -1
- package/dist/admin/client/collection/sortable-header.svelte +33 -9
- package/dist/admin/client/collection/state-display.svelte +102 -0
- package/dist/admin/client/collection/state-display.svelte.d.ts +12 -0
- package/dist/admin/client/collection/status-badge.svelte +99 -11
- package/dist/admin/client/collection/status-badge.svelte.d.ts +15 -1
- package/dist/admin/client/collection/table-pagination.svelte +21 -6
- package/dist/admin/client/collection/table-toolbar.svelte +105 -80
- package/dist/admin/client/collection/table-toolbar.svelte.d.ts +11 -8
- package/dist/admin/client/entry/entry-form.svelte +36 -11
- package/dist/admin/client/entry/entry-form.svelte.d.ts +1 -0
- package/dist/admin/client/entry/entry-header.svelte +22 -15
- package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -0
- package/dist/admin/client/entry/entry.svelte +269 -165
- package/dist/admin/client/entry/header/a11y-header-badge.svelte +47 -0
- package/dist/admin/client/entry/header/a11y-header-badge.svelte.d.ts +8 -0
- package/dist/admin/client/entry/header/publish-panel.svelte +69 -13
- package/dist/admin/client/entry/header/save-indicator.svelte +57 -28
- package/dist/admin/client/entry/header/save-indicator.svelte.d.ts +1 -0
- package/dist/admin/client/entry/header/status-badge.svelte +60 -15
- package/dist/admin/client/entry/header/status-badge.svelte.d.ts +1 -2
- package/dist/admin/client/entry/header/version-history-sheet.svelte +1 -1
- package/dist/admin/client/entry/hybrid/hybrid-layout.svelte +74 -23
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +1 -1
- package/dist/admin/client/entry/utils.d.ts +14 -0
- package/dist/admin/client/entry/utils.js +28 -0
- package/dist/admin/client/form/form-submission/form-submission.svelte +2 -2
- package/dist/admin/client/form/form-submissions.svelte +143 -194
- package/dist/admin/client/form/form-submissions.svelte.d.ts +2 -0
- package/dist/admin/client/login/lang.d.ts +3 -0
- package/dist/admin/client/login/lang.js +10 -4
- package/dist/admin/client/login/login-form.svelte +8 -1
- package/dist/admin/client/login/reset-password-page.svelte +24 -3
- package/dist/admin/client/login/schema.d.ts +14 -2
- package/dist/admin/client/login/schema.js +19 -8
- package/dist/admin/client/maintenance/maintenance-page.svelte +16 -17
- package/dist/admin/client/media/media-page.svelte +1 -1
- package/dist/admin/client/shop/coupon-edit-page.svelte +117 -13
- package/dist/admin/client/shop/coupon-form.svelte +282 -138
- package/dist/admin/client/shop/coupon-form.svelte.d.ts +1 -9
- package/dist/admin/client/shop/coupon-new-page.svelte +40 -10
- package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +2 -17
- package/dist/admin/client/shop/coupon-schema.d.ts +28 -0
- package/dist/admin/client/shop/coupon-schema.js +53 -0
- package/dist/admin/client/shop/coupons-list-page.svelte +262 -118
- package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +16 -1
- package/dist/admin/client/shop/shipping-method-edit-page.svelte +108 -59
- package/dist/admin/client/shop/shipping-method-form.svelte +36 -9
- package/dist/admin/client/shop/shipping-method-new-page.svelte +44 -13
- package/dist/admin/client/shop/shipping-methods-list-page.svelte +101 -59
- package/dist/admin/client/shop/shop-order-detail-page.svelte +113 -84
- package/dist/admin/client/shop/shop-orders-list-page.svelte +302 -152
- package/dist/admin/client/shop/shop-orders-list-page.svelte.d.ts +18 -1
- package/dist/admin/client/shop/shop-products-list-page.svelte +355 -118
- package/dist/admin/client/shop/shop-products-list-page.svelte.d.ts +19 -1
- package/dist/admin/client/users/accept-invite-page.svelte +24 -3
- package/dist/admin/client/users/create-user-dialog.svelte +3 -8
- package/dist/admin/client/users/lang.d.ts +2 -0
- package/dist/admin/client/users/lang.js +4 -0
- package/dist/admin/client/users/pending-invitations.svelte +2 -9
- package/dist/admin/client/users/user-name-cell.svelte +20 -0
- package/dist/admin/client/users/user-name-cell.svelte.d.ts +9 -0
- package/dist/admin/client/users/user-role-badge.svelte +16 -0
- package/dist/admin/client/users/user-role-badge.svelte.d.ts +7 -0
- package/dist/admin/client/users/user-row-actions.svelte +72 -0
- package/dist/admin/client/users/user-row-actions.svelte.d.ts +20 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte +2 -11
- package/dist/admin/client/users/users-page.svelte +283 -497
- package/dist/admin/client/users/users-page.svelte.d.ts +12 -1
- package/dist/admin/components/dashboard/form-submissions-widget.svelte +59 -74
- package/dist/admin/components/dashboard/recent-activity.svelte +17 -5
- package/dist/admin/components/dashboard/recent-entries.svelte +19 -7
- package/dist/admin/components/dialogs/confirmation-dialog.svelte +105 -0
- package/dist/admin/components/dialogs/confirmation-dialog.svelte.d.ts +13 -0
- package/dist/admin/components/fields/block-picker-modal.svelte +6 -0
- package/dist/admin/components/fields/blocks-field.svelte +46 -1
- package/dist/admin/components/fields/boolean-field.svelte +1 -1
- package/dist/admin/components/fields/field-renderer.svelte +23 -21
- package/dist/admin/components/fields/file-field.svelte +344 -30
- package/dist/admin/components/fields/media-field.svelte +16 -2
- package/dist/admin/components/fields/radio-field.svelte +22 -0
- package/dist/admin/components/fields/relation-field.svelte +123 -97
- package/dist/admin/components/fields/relation-picker-dialog.svelte +2 -2
- package/dist/admin/components/fields/seo-field.svelte +60 -30
- package/dist/admin/components/fields/shop-field.svelte +9 -4
- package/dist/admin/components/fields/simple-array-field.svelte +321 -151
- package/dist/admin/components/fields/simple-array-field.svelte.d.ts +3 -0
- package/dist/admin/components/fields/slug-field.svelte +146 -21
- package/dist/admin/components/fields/text-field-wrapper.svelte +37 -20
- package/dist/admin/components/fields/text-field.svelte +7 -2
- package/dist/admin/components/fields/url-field-wrapper.svelte +10 -0
- package/dist/admin/components/fields/url-field.svelte +36 -23
- package/dist/admin/components/forms/form-error-summary.svelte +143 -0
- package/dist/admin/components/forms/form-error-summary.svelte.d.ts +27 -0
- package/dist/admin/components/layout/app-sidebar.svelte +7 -2
- package/dist/admin/components/layout/detail-page-shell.svelte +71 -0
- package/dist/admin/components/layout/detail-page-shell.svelte.d.ts +24 -0
- package/dist/admin/components/layout/lang.d.ts +5 -0
- package/dist/admin/components/layout/lang.js +10 -0
- package/dist/admin/components/layout/layout-renderer.svelte +71 -2
- package/dist/admin/components/layout/layout-renderer.svelte.d.ts +1 -0
- package/dist/admin/components/layout/layout-tabs.svelte +172 -0
- package/dist/admin/components/layout/layout-tabs.svelte.d.ts +24 -0
- package/dist/admin/components/layout/nav-breadcrumbs.svelte +25 -7
- package/dist/admin/components/layout/nav-collections.svelte +23 -36
- package/dist/admin/components/layout/nav-forms.svelte +19 -35
- package/dist/admin/components/layout/nav-main.svelte +3 -28
- package/dist/admin/components/layout/nav-search.svelte +70 -2
- package/dist/admin/components/layout/nav-section.svelte +77 -0
- package/dist/admin/components/layout/nav-section.svelte.d.ts +22 -0
- package/dist/admin/components/layout/nav-shop.svelte +3 -27
- package/dist/admin/components/layout/nav-singletons.svelte +16 -28
- package/dist/admin/components/layout/page-header.stories.svelte +93 -0
- package/dist/admin/components/layout/page-header.stories.svelte.d.ts +27 -0
- package/dist/admin/components/layout/page-header.svelte +68 -0
- package/dist/admin/components/layout/page-header.svelte.d.ts +17 -0
- package/dist/admin/components/layout/site-header.svelte +9 -0
- package/dist/admin/components/layout/site-header.svelte.d.ts +2 -17
- package/dist/admin/components/media/file/file-name-input.svelte +6 -2
- package/dist/admin/components/media/file/file-preview.svelte +130 -17
- package/dist/admin/components/media/file-upload.svelte +16 -7
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/files-list.svelte +153 -53
- package/dist/admin/components/media/files-list.svelte.d.ts +1 -0
- package/dist/admin/components/media/media-library.svelte +577 -198
- package/dist/admin/components/media/media-library.svelte.d.ts +4 -0
- package/dist/admin/components/media/media-selector.svelte +4 -2
- package/dist/admin/components/media/media-selector.svelte.d.ts +1 -0
- package/dist/admin/components/media/tag-sidebar.svelte +4 -4
- package/dist/admin/components/tiptap/FigureNodeView.svelte +10 -0
- package/dist/admin/components/tiptap/bubble-menu.svelte +104 -0
- package/dist/admin/components/tiptap/bubble-menu.svelte.d.ts +19 -0
- package/dist/admin/components/tiptap/content-editor.svelte +28 -24
- package/dist/admin/components/tiptap/editor-toolbar.svelte +7 -7
- package/dist/admin/components/tiptap/extensions.js +5 -1
- package/dist/admin/components/tiptap/image-dialog.svelte +5 -1
- package/dist/admin/components/tiptap/link-dialog.svelte +2 -0
- package/dist/admin/components/tiptap/tiptap-editor.svelte +18 -20
- package/dist/admin/components/tiptap/video-dialog.svelte +1 -1
- package/dist/admin/i18n/errors.d.ts +140 -0
- package/dist/admin/i18n/errors.js +151 -0
- package/dist/admin/remote/entry.remote.d.ts +59 -4
- package/dist/admin/remote/entry.remote.js +239 -62
- package/dist/admin/remote/shop.remote.d.ts +37 -32
- package/dist/admin/remote/shop.remote.js +9 -2
- package/dist/admin/shared/password-generate.d.ts +6 -0
- package/dist/admin/shared/password-generate.js +40 -0
- package/dist/admin/shared/password-schema.d.ts +6 -0
- package/dist/admin/shared/password-schema.js +10 -3
- package/dist/admin/styles/admin.css +23 -6
- package/dist/admin/styles/tokens.md +244 -0
- package/dist/admin/utils/accordionActivation.d.ts +13 -0
- package/dist/admin/utils/accordionActivation.js +35 -0
- package/dist/admin/utils/entryLabel.d.ts +23 -0
- package/dist/admin/utils/entryLabel.js +51 -12
- package/dist/admin/utils/field-a11y.d.ts +29 -0
- package/dist/admin/utils/field-a11y.js +23 -0
- package/dist/admin/utils/fieldPathElement.d.ts +9 -0
- package/dist/admin/utils/fieldPathElement.js +18 -0
- package/dist/admin/utils/fileDisplay.d.ts +10 -0
- package/dist/admin/utils/fileDisplay.js +26 -0
- package/dist/admin/utils/flattenFormErrors.d.ts +19 -0
- package/dist/admin/utils/flattenFormErrors.js +102 -0
- package/dist/admin/utils/formatters.d.ts +12 -0
- package/dist/admin/utils/{formatDate.js → formatters.js} +23 -2
- package/dist/admin/utils/scrollWithin.d.ts +9 -0
- package/dist/admin/utils/scrollWithin.js +32 -0
- package/dist/admin/utils/tabActivation.d.ts +12 -0
- package/dist/admin/utils/tabActivation.js +24 -0
- package/dist/cms/runtime/schema.d.ts +1 -0
- package/dist/cms/runtime/schema.js +1 -0
- package/dist/cms/runtime/types.d.ts +80 -7
- package/dist/components/ui/accordion/accordion-content.svelte +17 -3
- package/dist/components/ui/accordion/accordion.stories.svelte +21 -1
- package/dist/components/ui/alert/alert.stories.svelte +14 -0
- package/dist/components/ui/alert-dialog/alert-dialog.stories.svelte +45 -0
- package/dist/components/ui/alert-dialog/alert-dialog.stories.svelte.d.ts +27 -0
- package/dist/components/ui/avatar/avatar.stories.svelte +27 -0
- package/dist/components/ui/badge/badge.stories.svelte +15 -0
- package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte +47 -0
- package/dist/components/ui/breadcrumb/breadcrumb.svelte +1 -1
- package/dist/components/ui/button/button.stories.svelte +53 -6
- package/dist/components/ui/button/button.svelte +39 -5
- package/dist/components/ui/button/button.svelte.d.ts +4 -0
- package/dist/components/ui/button-group/button-group.stories.svelte +44 -0
- package/dist/components/ui/button-group/button-group.stories.svelte.d.ts +27 -0
- package/dist/components/ui/calendar/calendar.stories.svelte +36 -0
- package/dist/components/ui/calendar/calendar.stories.svelte.d.ts +27 -0
- package/dist/components/ui/card/card.stories.svelte +7 -0
- package/dist/components/ui/carousel/carousel.stories.svelte +43 -0
- package/dist/components/ui/carousel/carousel.stories.svelte.d.ts +27 -0
- package/dist/components/ui/checkbox/checkbox.stories.svelte +67 -0
- package/dist/components/ui/checkbox/checkbox.stories.svelte.d.ts +27 -0
- package/dist/components/ui/checkbox/checkbox.svelte +3 -3
- package/dist/components/ui/command/command.stories.svelte +18 -0
- package/dist/components/ui/data-table/data-table.stories.svelte +61 -0
- package/dist/components/ui/data-table/data-table.stories.svelte.d.ts +18 -0
- package/dist/components/ui/dialog/dialog-content.svelte +5 -0
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +2 -0
- package/dist/components/ui/dialog/dialog.stories.svelte +35 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.stories.svelte +74 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.stories.svelte.d.ts +27 -0
- package/dist/components/ui/field/field-context.svelte.d.ts +22 -0
- package/dist/components/ui/field/field-context.svelte.js +9 -0
- package/dist/components/ui/field/field-control.svelte +18 -0
- package/dist/components/ui/field/field-control.svelte.d.ts +8 -0
- package/dist/components/ui/field/field-description.svelte +12 -0
- package/dist/components/ui/field/field-error.svelte +14 -6
- package/dist/components/ui/field/field-label.svelte +10 -0
- package/dist/components/ui/field/field.stories.svelte +95 -9
- package/dist/components/ui/field/field.svelte +57 -0
- package/dist/components/ui/field/field.svelte.d.ts +2 -0
- package/dist/components/ui/field/index.d.ts +3 -1
- package/dist/components/ui/field/index.js +4 -2
- package/dist/components/ui/form/form-field-errors.svelte +1 -1
- package/dist/components/ui/form/form.stories.svelte +25 -0
- package/dist/components/ui/form/form.stories.svelte.d.ts +26 -0
- package/dist/components/ui/input/input.stories.svelte +26 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group.stories.svelte +43 -0
- package/dist/components/ui/input-group/input-group.stories.svelte.d.ts +27 -0
- package/dist/components/ui/item/item.stories.svelte +61 -0
- package/dist/components/ui/item/item.stories.svelte.d.ts +27 -0
- package/dist/components/ui/label/label.stories.svelte +7 -0
- package/dist/components/ui/live-region/index.d.ts +1 -0
- package/dist/components/ui/live-region/index.js +1 -0
- package/dist/components/ui/live-region/live-region-demo.svelte +32 -0
- package/dist/components/ui/live-region/live-region-demo.svelte.d.ts +7 -0
- package/dist/components/ui/live-region/live-region.stories.svelte +23 -0
- package/dist/components/ui/live-region/live-region.stories.svelte.d.ts +26 -0
- package/dist/components/ui/live-region/live-region.svelte +12 -0
- package/dist/components/ui/live-region/live-region.svelte.d.ts +8 -0
- package/dist/components/ui/popover/popover.stories.svelte +34 -0
- package/dist/components/ui/radio-group/radio-group.stories.svelte +58 -0
- package/dist/components/ui/radio-group/radio-group.stories.svelte.d.ts +27 -0
- package/dist/components/ui/resizable/resizable.stories.svelte +56 -0
- package/dist/components/ui/resizable/resizable.stories.svelte.d.ts +27 -0
- package/dist/components/ui/select/select.stories.svelte +49 -0
- package/dist/components/ui/separator/separator.stories.svelte +18 -0
- package/dist/components/ui/sheet/sheet.stories.svelte +34 -0
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-menu-button.svelte +1 -0
- package/dist/components/ui/sidebar/sidebar-trigger.svelte +1 -1
- package/dist/components/ui/sidebar/sidebar.stories.svelte +72 -0
- package/dist/components/ui/sidebar/sidebar.stories.svelte.d.ts +27 -0
- package/dist/components/ui/skeleton/skeleton.stories.svelte +39 -0
- package/dist/components/ui/skeleton/skeleton.stories.svelte.d.ts +27 -0
- package/dist/components/ui/skeleton/skeleton.svelte +6 -0
- package/dist/components/ui/sonner/index.d.ts +1 -1
- package/dist/components/ui/sonner/index.js +1 -1
- package/dist/components/ui/sonner/sonner.stories.svelte +7 -0
- package/dist/components/ui/sonner/sonner.svelte +17 -1
- package/dist/components/ui/sonner/sonner.svelte.d.ts +6 -0
- package/dist/components/ui/spinner/spinner.stories.svelte +30 -0
- package/dist/components/ui/spinner/spinner.stories.svelte.d.ts +27 -0
- package/dist/components/ui/switch/switch.stories.svelte +56 -0
- package/dist/components/ui/switch/switch.stories.svelte.d.ts +27 -0
- package/dist/components/ui/table/table-cell.svelte +1 -1
- package/dist/components/ui/table/table-head.svelte +1 -1
- package/dist/components/ui/table/table.stories.svelte +68 -0
- package/dist/components/ui/table/table.stories.svelte.d.ts +27 -0
- package/dist/components/ui/table/table.svelte +1 -1
- package/dist/components/ui/tabs/tabs.stories.svelte +48 -0
- package/dist/components/ui/tabs/tabs.stories.svelte.d.ts +27 -0
- package/dist/components/ui/textarea/textarea.stories.svelte +21 -0
- package/dist/components/ui/toggle/toggle.stories.svelte +23 -0
- package/dist/components/ui/toggle-group/toggle-group.stories.svelte +43 -0
- package/dist/components/ui/tooltip/tooltip.stories.svelte +46 -6
- package/dist/core/fields/fieldSchemaToTs.d.ts +7 -0
- package/dist/core/fields/fieldSchemaToTs.js +234 -90
- package/dist/core/fields/layoutUtils.d.ts +4 -1
- package/dist/core/fields/layoutUtils.js +41 -4
- package/dist/core/fields/resolveSeo.d.ts +70 -0
- package/dist/core/fields/resolveSeo.js +88 -0
- package/dist/core/fields/seoFieldDescriptor.d.ts +43 -0
- package/dist/core/fields/seoFieldDescriptor.js +74 -0
- package/dist/core/fields/slugPath.d.ts +13 -0
- package/dist/core/fields/slugPath.js +32 -0
- package/dist/core/fields/urlUtils.d.ts +8 -0
- package/dist/core/fields/urlUtils.js +27 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/server/entries/operations/create.js +13 -0
- package/dist/core/server/entries/operations/get.d.ts +7 -0
- package/dist/core/server/entries/operations/get.js +10 -6
- package/dist/core/server/entries/operations/slugUniqueness.d.ts +37 -0
- package/dist/core/server/entries/operations/slugUniqueness.js +116 -0
- package/dist/core/server/entries/operations/update.d.ts +6 -1
- package/dist/core/server/entries/operations/update.js +24 -1
- package/dist/core/server/fields/slugResolver.d.ts +3 -13
- package/dist/core/server/fields/slugResolver.js +8 -37
- package/dist/core/server/generator/fields.js +10 -17
- package/dist/core/server/generator/formFields.js +2 -1
- package/dist/core/server/generator/generator.js +4 -4
- package/dist/core/server/generator/utils.d.ts +1 -0
- package/dist/core/server/generator/utils.js +4 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/shop/server/orders.d.ts +1 -0
- package/dist/shop/server/orders.js +14 -0
- package/dist/shop/server/shop-data.d.ts +2 -0
- package/dist/shop/server/shop-data.js +20 -5
- package/dist/sveltekit/server/handle.js +17 -0
- package/dist/types/cms.schema.js +4 -2
- package/dist/types/fields.d.ts +35 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/layout.d.ts +35 -2
- package/dist/updates/0.26.0/index.d.ts +2 -0
- package/dist/updates/0.26.0/index.js +51 -0
- package/dist/updates/index.js +3 -1
- package/package.json +29 -7
- package/dist/admin/client/collection/empty-state.svelte +0 -28
- package/dist/admin/client/collection/empty-state.svelte.d.ts +0 -9
- package/dist/admin/client/form/submission-status-badge.svelte +0 -41
- package/dist/admin/client/form/submission-status-badge.svelte.d.ts +0 -7
- package/dist/admin/components/media/file-preview.svelte +0 -51
- package/dist/admin/components/media/file-preview.svelte.d.ts +0 -6
- package/dist/admin/utils/formatDate.d.ts +0 -5
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const SEO_KEYS = [
|
|
2
|
+
'slug',
|
|
3
|
+
'canonicalUrl',
|
|
4
|
+
'title',
|
|
5
|
+
'description',
|
|
6
|
+
'ogImage',
|
|
7
|
+
'keywords',
|
|
8
|
+
'customCode'
|
|
9
|
+
];
|
|
10
|
+
// Compile-time exhaustiveness guard: fails the build if `SeoFieldData` gains or
|
|
11
|
+
// loses a key relative to `SEO_KEYS`, instead of a key silently dropping from
|
|
12
|
+
// the resolver. Must reference every `SeoFieldData` key and compile today.
|
|
13
|
+
const _SEO_KEYS_EXHAUSTIVE = {
|
|
14
|
+
slug: true,
|
|
15
|
+
canonicalUrl: true,
|
|
16
|
+
title: true,
|
|
17
|
+
description: true,
|
|
18
|
+
ogImage: true,
|
|
19
|
+
keywords: true,
|
|
20
|
+
customCode: true
|
|
21
|
+
};
|
|
22
|
+
void _SEO_KEYS_EXHAUSTIVE;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve `entry.seo` into flat fields, flattening localized objects by
|
|
25
|
+
* `language`.
|
|
26
|
+
*
|
|
27
|
+
* For each of the six text keys: a `string` value passes through; a localized
|
|
28
|
+
* `{ lang: string }` object resolves to `obj[language]` (or `undefined` if the
|
|
29
|
+
* requested language is missing); anything else resolves to `undefined`.
|
|
30
|
+
*
|
|
31
|
+
* `ogImage` is exempt from flattening: it passes through unchanged as a string
|
|
32
|
+
* or a {@link MediaFile} object (only `null`/`undefined` → `undefined`).
|
|
33
|
+
*
|
|
34
|
+
* The name `resolveSeo` is a deliberate, v1.0-frozen choice — it aligns with
|
|
35
|
+
* the `resolveEntryUrl` / `resolveMedia*` resolver family.
|
|
36
|
+
*
|
|
37
|
+
* @param entry - The (possibly resolved/hydrated) entry, or any object with an
|
|
38
|
+
* optional `seo` property. A non-object or missing `seo` yields all-`undefined`.
|
|
39
|
+
* @param language - The active language code. Required to flatten localized
|
|
40
|
+
* `{ lang: string }` values; localized fields resolve to `undefined` without it.
|
|
41
|
+
* @returns A {@link ResolvedSeo} — all keys present, each `string`/`MediaFile`
|
|
42
|
+
* (only `ogImage`) or `undefined`.
|
|
43
|
+
* @public
|
|
44
|
+
* @example
|
|
45
|
+
* ```svelte
|
|
46
|
+
* <script lang="ts">
|
|
47
|
+
* import { resolveSeo } from 'includio-cms/core';
|
|
48
|
+
* import { Seo } from 'includio-cms';
|
|
49
|
+
* let { entry, language } = $props();
|
|
50
|
+
* const seo = $derived(resolveSeo(entry, language));
|
|
51
|
+
* </script>
|
|
52
|
+
*
|
|
53
|
+
* <svelte:head>
|
|
54
|
+
* {#if seo.title}<title>{seo.title}</title>{/if}
|
|
55
|
+
* {#if seo.canonicalUrl}<link rel="canonical" href={seo.canonicalUrl} />{/if}
|
|
56
|
+
* </svelte:head>
|
|
57
|
+
*
|
|
58
|
+
* <Seo
|
|
59
|
+
* title={seo.title}
|
|
60
|
+
* description={seo.description}
|
|
61
|
+
* keywords={seo.keywords}
|
|
62
|
+
* ogImage={seo.ogImage}
|
|
63
|
+
* canonicalUrl={seo.canonicalUrl}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function resolveSeo(entry, language) {
|
|
68
|
+
const rawSeo = entry.seo;
|
|
69
|
+
const seo = rawSeo != null && typeof rawSeo === 'object' ? rawSeo : {};
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const k of SEO_KEYS) {
|
|
72
|
+
const v = seo[k];
|
|
73
|
+
if (k === 'ogImage') {
|
|
74
|
+
// Never localize/flatten: pass string OR MediaFile object through.
|
|
75
|
+
out[k] = v != null && (typeof v === 'string' || typeof v === 'object') ? v : undefined;
|
|
76
|
+
}
|
|
77
|
+
else if (typeof v === 'string') {
|
|
78
|
+
out[k] = v;
|
|
79
|
+
}
|
|
80
|
+
else if (language && v != null && typeof v === 'object') {
|
|
81
|
+
out[k] = v[language];
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
out[k] = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* @internal v1.0-frozen SEO field shape. SSOT for Zod + TS gen. Mismatches
|
|
4
|
+
* (description/ogImage required-in-Zod vs optional-in-TS; ogImage flat-not-localized)
|
|
5
|
+
* are PRE-EXISTING and preserved 1:1. Fixing = behavior change, post-1.0.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: the current Zod `z.object` key order and the current TS template-literal
|
|
8
|
+
* line order DIFFER (Zod: ...keywords, ogImage, customCode; TS: ...ogImage,
|
|
9
|
+
* keywords, customCode). This array uses the ZOD source order; `buildSeoTsType`
|
|
10
|
+
* keeps its own (TS) line order verbatim and only derives `?` per key from
|
|
11
|
+
* `tsOptional`. The Zod shape-keys snapshot sorts keys; the TS-type-string
|
|
12
|
+
* snapshot preserves line order verbatim — so both orderings are deliberately
|
|
13
|
+
* reproduced 1:1.
|
|
14
|
+
*/
|
|
15
|
+
export interface SeoDescriptorEntry {
|
|
16
|
+
key: string;
|
|
17
|
+
/** non-localized Zod */
|
|
18
|
+
zod: 'string' | 'string-optional';
|
|
19
|
+
zodLocalized: 'perLang-string' | 'perLang-optional' | 'flat-string';
|
|
20
|
+
/** TS codegen */
|
|
21
|
+
tsOptional: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare const SEO_FIELD_DESCRIPTOR: readonly SeoDescriptorEntry[];
|
|
24
|
+
/**
|
|
25
|
+
* @internal Builds the exact object passed to `z.object(...)` for the SEO field,
|
|
26
|
+
* for both non-localized and localized modes — byte-identical to the previous
|
|
27
|
+
* hand-written blocks in `fieldSchemaToTs.ts`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildSeoZodShape(languages: string[], localized: boolean): Record<string, z.ZodType>;
|
|
30
|
+
/**
|
|
31
|
+
* @internal TS-codegen line order (verbatim from generator/fields.ts
|
|
32
|
+
* `case 'seo':`). Distinct from the Zod object key order above (see module
|
|
33
|
+
* note). Exported solely so drift-guard tests can assert it stays a
|
|
34
|
+
* permutation of the descriptor keys.
|
|
35
|
+
*/
|
|
36
|
+
export declare const SEO_TS_LINE_ORDER: readonly ["slug", "canonicalUrl", "title", "description", "ogImage", "keywords", "customCode"];
|
|
37
|
+
/**
|
|
38
|
+
* @internal Returns the SEO TS type string with EXACTLY the same characters
|
|
39
|
+
* (tabs/braces/whitespace) as the previous hand-written template literal in
|
|
40
|
+
* `server/generator/fields.ts`. Only the per-key `?` is derived from the
|
|
41
|
+
* descriptor's `tsOptional`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildSeoTsType(): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const SEO_FIELD_DESCRIPTOR = [
|
|
3
|
+
{ key: 'slug', zod: 'string', zodLocalized: 'perLang-string', tsOptional: false },
|
|
4
|
+
{
|
|
5
|
+
key: 'canonicalUrl',
|
|
6
|
+
zod: 'string-optional',
|
|
7
|
+
zodLocalized: 'perLang-optional',
|
|
8
|
+
tsOptional: true
|
|
9
|
+
},
|
|
10
|
+
{ key: 'title', zod: 'string', zodLocalized: 'perLang-string', tsOptional: false },
|
|
11
|
+
{ key: 'description', zod: 'string', zodLocalized: 'perLang-string', tsOptional: true },
|
|
12
|
+
{ key: 'keywords', zod: 'string-optional', zodLocalized: 'perLang-optional', tsOptional: true },
|
|
13
|
+
{ key: 'ogImage', zod: 'string', zodLocalized: 'flat-string', tsOptional: true },
|
|
14
|
+
{ key: 'customCode', zod: 'string-optional', zodLocalized: 'perLang-optional', tsOptional: true }
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* @internal Builds the exact object passed to `z.object(...)` for the SEO field,
|
|
18
|
+
* for both non-localized and localized modes — byte-identical to the previous
|
|
19
|
+
* hand-written blocks in `fieldSchemaToTs.ts`.
|
|
20
|
+
*/
|
|
21
|
+
export function buildSeoZodShape(languages, localized) {
|
|
22
|
+
const shape = {};
|
|
23
|
+
for (const entry of SEO_FIELD_DESCRIPTOR) {
|
|
24
|
+
if (!localized) {
|
|
25
|
+
shape[entry.key] = entry.zod === 'string-optional' ? z.string().optional() : z.string();
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
switch (entry.zodLocalized) {
|
|
29
|
+
case 'flat-string':
|
|
30
|
+
shape[entry.key] = z.string();
|
|
31
|
+
break;
|
|
32
|
+
case 'perLang-optional':
|
|
33
|
+
shape[entry.key] = z.object(Object.fromEntries(languages.map((lang) => [lang, z.string().optional()])));
|
|
34
|
+
break;
|
|
35
|
+
case 'perLang-string':
|
|
36
|
+
shape[entry.key] = z.object(Object.fromEntries(languages.map((lang) => [lang, z.string()])));
|
|
37
|
+
break;
|
|
38
|
+
default: {
|
|
39
|
+
const _exhaustive = entry.zodLocalized;
|
|
40
|
+
throw new Error(`Unhandled zodLocalized: ${String(_exhaustive)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return shape;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @internal TS-codegen line order (verbatim from generator/fields.ts
|
|
48
|
+
* `case 'seo':`). Distinct from the Zod object key order above (see module
|
|
49
|
+
* note). Exported solely so drift-guard tests can assert it stays a
|
|
50
|
+
* permutation of the descriptor keys.
|
|
51
|
+
*/
|
|
52
|
+
export const SEO_TS_LINE_ORDER = [
|
|
53
|
+
'slug',
|
|
54
|
+
'canonicalUrl',
|
|
55
|
+
'title',
|
|
56
|
+
'description',
|
|
57
|
+
'ogImage',
|
|
58
|
+
'keywords',
|
|
59
|
+
'customCode'
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* @internal Returns the SEO TS type string with EXACTLY the same characters
|
|
63
|
+
* (tabs/braces/whitespace) as the previous hand-written template literal in
|
|
64
|
+
* `server/generator/fields.ts`. Only the per-key `?` is derived from the
|
|
65
|
+
* descriptor's `tsOptional`.
|
|
66
|
+
*/
|
|
67
|
+
export function buildSeoTsType() {
|
|
68
|
+
const byKey = new Map(SEO_FIELD_DESCRIPTOR.map((e) => [e.key, e]));
|
|
69
|
+
const lines = SEO_TS_LINE_ORDER.map((key) => {
|
|
70
|
+
const entry = byKey.get(key);
|
|
71
|
+
return `\t\t\t\t${key}${entry.tsOptional ? '?' : ''}: string;`;
|
|
72
|
+
});
|
|
73
|
+
return `{\n${lines.join('\n')}\n\t\t\t}`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** @internal Pure: resolve slug dot-path, defaulting to 'seo.slug'. */
|
|
2
|
+
export declare function resolveSlugPath(slugField: string | undefined): string;
|
|
3
|
+
/** @internal Traverse dot-path; handles string & localized { lang: string }. */
|
|
4
|
+
export declare function getSlugFromEntryData(data: Record<string, unknown>, slugPath: string, language?: string): string | undefined;
|
|
5
|
+
/** @internal Pure: apply pathTemplate (no getCMS). */
|
|
6
|
+
export declare function applyPathTemplate(pathTemplate: string | undefined, slug: string): string;
|
|
7
|
+
/** @internal Compose full entry URL; undefined when no resolvable slug. */
|
|
8
|
+
export declare function resolveEntryUrl(opts: {
|
|
9
|
+
slugField?: string;
|
|
10
|
+
pathTemplate?: string;
|
|
11
|
+
data: Record<string, unknown>;
|
|
12
|
+
language?: string;
|
|
13
|
+
}): string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const DEFAULT_SLUG_PATH = 'seo.slug';
|
|
2
|
+
/** @internal Pure: resolve slug dot-path, defaulting to 'seo.slug'. */
|
|
3
|
+
export function resolveSlugPath(slugField) {
|
|
4
|
+
return slugField || DEFAULT_SLUG_PATH;
|
|
5
|
+
}
|
|
6
|
+
/** @internal Traverse dot-path; handles string & localized { lang: string }. */
|
|
7
|
+
export function getSlugFromEntryData(data, slugPath, language) {
|
|
8
|
+
const parts = slugPath.split('.');
|
|
9
|
+
let current = data;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
if (current == null || typeof current !== 'object')
|
|
12
|
+
return undefined;
|
|
13
|
+
current = current[part];
|
|
14
|
+
}
|
|
15
|
+
if (typeof current === 'string')
|
|
16
|
+
return current;
|
|
17
|
+
if (language && current != null && typeof current === 'object') {
|
|
18
|
+
return current[language];
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
/** @internal Pure: apply pathTemplate (no getCMS). */
|
|
23
|
+
export function applyPathTemplate(pathTemplate, slug) {
|
|
24
|
+
return pathTemplate ? pathTemplate.replace('{slug}', slug) : slug;
|
|
25
|
+
}
|
|
26
|
+
/** @internal Compose full entry URL; undefined when no resolvable slug. */
|
|
27
|
+
export function resolveEntryUrl(opts) {
|
|
28
|
+
const slug = getSlugFromEntryData(opts.data, resolveSlugPath(opts.slugField), opts.language);
|
|
29
|
+
if (!slug)
|
|
30
|
+
return undefined;
|
|
31
|
+
return applyPathTemplate(opts.pathTemplate, slug);
|
|
32
|
+
}
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Check if URL is external (starts with http:// or https://).
|
|
3
3
|
*/
|
|
4
4
|
export declare function isExternalUrl(url: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Validate a URL field string value. Empty is valid (presence is enforced by
|
|
7
|
+
* `required`). Accepts absolute http(s) URLs, `mailto:` / `tel:`, and any
|
|
8
|
+
* scheme-less value (root-relative `/path`, anchor `#id`, or a bare internal
|
|
9
|
+
* slug like `witaj` — many non-technical editors omit the leading slash).
|
|
10
|
+
* Rejects whitespace and disallowed schemes (e.g. `javascript:`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isValidUrlValue(value: string): boolean;
|
|
5
13
|
/**
|
|
6
14
|
* Build rel attribute string from individual tokens.
|
|
7
15
|
* Deduplicates and returns space-separated string or empty string.
|
|
@@ -4,6 +4,33 @@
|
|
|
4
4
|
export function isExternalUrl(url) {
|
|
5
5
|
return /^https?:\/\//i.test(url);
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Validate a URL field string value. Empty is valid (presence is enforced by
|
|
9
|
+
* `required`). Accepts absolute http(s) URLs, `mailto:` / `tel:`, and any
|
|
10
|
+
* scheme-less value (root-relative `/path`, anchor `#id`, or a bare internal
|
|
11
|
+
* slug like `witaj` — many non-technical editors omit the leading slash).
|
|
12
|
+
* Rejects whitespace and disallowed schemes (e.g. `javascript:`).
|
|
13
|
+
*/
|
|
14
|
+
export function isValidUrlValue(value) {
|
|
15
|
+
const v = value.trim();
|
|
16
|
+
if (v === '')
|
|
17
|
+
return true;
|
|
18
|
+
if (/\s/.test(value))
|
|
19
|
+
return false;
|
|
20
|
+
const schemeMatch = v.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
21
|
+
if (!schemeMatch)
|
|
22
|
+
return true; // scheme-less → internal path/slug/anchor
|
|
23
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
24
|
+
if (scheme === 'mailto' || scheme === 'tel')
|
|
25
|
+
return true;
|
|
26
|
+
try {
|
|
27
|
+
const u = new URL(v);
|
|
28
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
7
34
|
/**
|
|
8
35
|
* Build rel attribute string from individual tokens.
|
|
9
36
|
* Deduplicates and returns space-separated string or empty string.
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { getCMS } from './cms.js';
|
|
2
2
|
export { resolveMediaWithStyles, type ResolvedMedia } from './server/fields/utils/resolveMedia.js';
|
|
3
|
+
export { resolveSeo, type ResolvedSeo } from './fields/resolveSeo.js';
|
|
3
4
|
export { createEntityAPI } from '../entity/index.js';
|
|
4
5
|
export { getAuth } from '../server/auth.js';
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { getCMS } from './cms.js';
|
|
2
2
|
export { resolveMediaWithStyles } from './server/fields/utils/resolveMedia.js';
|
|
3
|
+
export { resolveSeo } from './fields/resolveSeo.js';
|
|
3
4
|
export { createEntityAPI } from '../entity/index.js';
|
|
4
5
|
export { getAuth } from '../server/auth.js';
|
|
@@ -6,6 +6,7 @@ import { _getDbEntryOrThrow as getDbEntryOrThrow } from './get.js';
|
|
|
6
6
|
import { generateZodSchemaFromFields } from '../../../fields/fieldSchemaToTs.js';
|
|
7
7
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
8
8
|
import { CmsError, formatZodDataIssues } from '../../../errors.js';
|
|
9
|
+
import { assertSlugUnique } from './slugUniqueness.js';
|
|
9
10
|
export const createEntrySchema = z.object({
|
|
10
11
|
slug: z.string(),
|
|
11
12
|
type: z.enum(entryTypes)
|
|
@@ -51,6 +52,18 @@ export const createEntryVersion = async (data, options) => {
|
|
|
51
52
|
}
|
|
52
53
|
validatedData = parsedData.data;
|
|
53
54
|
}
|
|
55
|
+
// Slug uniqueness: only enforced for published-or-scheduled writes (draft
|
|
56
|
+
// versions and skipValidation paths — e.g. seeds — are exempt). Each slug
|
|
57
|
+
// field is checked in its own namespace (see assertSlugUnique).
|
|
58
|
+
if (data.publishedAt != null && !options?.skipValidation) {
|
|
59
|
+
const entry = await getDbEntryOrThrow({ id: data.entryId });
|
|
60
|
+
await assertSlugUnique({
|
|
61
|
+
collection: entry.slug,
|
|
62
|
+
lang: data.lang,
|
|
63
|
+
data: validatedData,
|
|
64
|
+
excludeEntryId: data.entryId
|
|
65
|
+
});
|
|
66
|
+
}
|
|
54
67
|
const newEntryVersion = await getCMS().databaseAdapter.createEntryVersion({
|
|
55
68
|
...data,
|
|
56
69
|
createdBy: user.id,
|
|
@@ -17,6 +17,13 @@ export declare const getEntryLabels: (options: {
|
|
|
17
17
|
search?: string;
|
|
18
18
|
limit?: number;
|
|
19
19
|
status?: "published" | "draft" | "all";
|
|
20
|
+
/**
|
|
21
|
+
* Preferred language for entry titles. Falls back to other languages when
|
|
22
|
+
* an entry has no version in the preferred one (links are language-
|
|
23
|
+
* agnostic — we resolve by id, but the human label should follow the
|
|
24
|
+
* editor's content language).
|
|
25
|
+
*/
|
|
26
|
+
language?: string;
|
|
20
27
|
}) => Promise<{
|
|
21
28
|
id: string;
|
|
22
29
|
label: string;
|
|
@@ -121,18 +121,22 @@ export const getEntryLabels = async (options) => {
|
|
|
121
121
|
if (dbEntries.length === 0)
|
|
122
122
|
return [];
|
|
123
123
|
const entryIds = dbEntries.map((e) => e.id);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
// Scan the preferred language first, then the rest as fallback. Without
|
|
125
|
+
// this an entry that only has a `pl` version was invisible to a `pl`
|
|
126
|
+
// editor because cms.languages[0] = 'en' in demo.
|
|
127
|
+
const langs = [
|
|
128
|
+
...(options.language ? [options.language] : []),
|
|
129
|
+
...cms.languages.filter((l) => l !== options.language)
|
|
130
|
+
];
|
|
131
|
+
const allVersions = (await Promise.all(langs.map((lang) => cms.databaseAdapter.getEntryVersions({ entryIds, lang }).then((vs) => vs.map((v) => ({ ...v, _langOrder: langs.indexOf(lang) })))))).flat();
|
|
129
132
|
const now = new Date();
|
|
130
133
|
const statusFilter = options.status ?? 'all';
|
|
131
134
|
const entryAdminTitle = config.entryAdminTitle;
|
|
132
135
|
let results = dbEntries
|
|
133
136
|
.map((entry) => {
|
|
134
137
|
const entryVersions = allVersions.filter((v) => v.entryId === entry.id);
|
|
135
|
-
|
|
138
|
+
// Preferred language wins; within a language, the latest version.
|
|
139
|
+
const sorted = entryVersions.sort((a, b) => a._langOrder - b._langOrder || b.versionNumber - a.versionNumber);
|
|
136
140
|
const publishedVersion = sorted.find((v) => v.publishedAt != null && v.publishedAt <= now) || null;
|
|
137
141
|
const hasPublished = publishedVersion != null;
|
|
138
142
|
if (statusFilter === 'published' && !hasPublished)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Field } from '../../../../types/fields.js';
|
|
2
|
+
/**
|
|
3
|
+
* Collect every dot-path in a collection whose value should be unique across
|
|
4
|
+
* entries (per content language) when published.
|
|
5
|
+
*
|
|
6
|
+
* Rule (decision: per-path, not per-collection):
|
|
7
|
+
* - Each `SlugField` (root or nested inside an object) has its own namespace.
|
|
8
|
+
* Two entries' top-level `slug` must not collide, but `slug` (entry A) and
|
|
9
|
+
* `seo.slug` (entry B) MAY share a value — they identify different things.
|
|
10
|
+
* - A `SeoField` always contributes its own `<path>.slug` namespace.
|
|
11
|
+
*
|
|
12
|
+
* Recurses into `object` fields; skips `array`/`blocks` because identifiers
|
|
13
|
+
* inside repeatable items aren't entry-level slugs.
|
|
14
|
+
*/
|
|
15
|
+
export declare function collectSlugFieldPaths(fields: Field[], prefix?: string): string[];
|
|
16
|
+
export interface AssertSlugUniqueArgs {
|
|
17
|
+
/** Collection slug (config slug, e.g. `'blog-post'`). */
|
|
18
|
+
collection: string;
|
|
19
|
+
/** Content language whose slugs must not collide. */
|
|
20
|
+
lang: string;
|
|
21
|
+
/** Validated entry data about to be persisted as a published version. */
|
|
22
|
+
data: Record<string, unknown>;
|
|
23
|
+
/** Entry being published — excluded from the conflict scan. */
|
|
24
|
+
excludeEntryId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reject publishing when any slug-typed field (or `seo.slug`) collides with
|
|
28
|
+
* another entry's currently-published-or-scheduled version in the same
|
|
29
|
+
* collection + language. Each slug path is checked in its own namespace.
|
|
30
|
+
* Drafts, archived entries, the entry's own history, and other languages
|
|
31
|
+
* are ignored. Empty slug values are skipped — uniqueness is only
|
|
32
|
+
* meaningful for non-empty strings.
|
|
33
|
+
*
|
|
34
|
+
* @throws CmsError SLUG_DUPLICATE on the first conflict (`details.path` says
|
|
35
|
+
* which slug-field collided).
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertSlugUnique({ collection, lang, data, excludeEntryId }: AssertSlugUniqueArgs): Promise<void>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { getCMS } from '../../../cms.js';
|
|
2
|
+
import { CmsError } from '../../../errors.js';
|
|
3
|
+
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Collect every dot-path in a collection whose value should be unique across
|
|
6
|
+
* entries (per content language) when published.
|
|
7
|
+
*
|
|
8
|
+
* Rule (decision: per-path, not per-collection):
|
|
9
|
+
* - Each `SlugField` (root or nested inside an object) has its own namespace.
|
|
10
|
+
* Two entries' top-level `slug` must not collide, but `slug` (entry A) and
|
|
11
|
+
* `seo.slug` (entry B) MAY share a value — they identify different things.
|
|
12
|
+
* - A `SeoField` always contributes its own `<path>.slug` namespace.
|
|
13
|
+
*
|
|
14
|
+
* Recurses into `object` fields; skips `array`/`blocks` because identifiers
|
|
15
|
+
* inside repeatable items aren't entry-level slugs.
|
|
16
|
+
*/
|
|
17
|
+
export function collectSlugFieldPaths(fields, prefix = '') {
|
|
18
|
+
const paths = [];
|
|
19
|
+
for (const f of fields) {
|
|
20
|
+
const full = prefix ? `${prefix}.${f.slug}` : f.slug;
|
|
21
|
+
if (f.type === 'slug') {
|
|
22
|
+
paths.push(full);
|
|
23
|
+
}
|
|
24
|
+
else if (f.type === 'seo') {
|
|
25
|
+
paths.push(`${full}.slug`);
|
|
26
|
+
}
|
|
27
|
+
else if (f.type === 'object' && 'fields' in f) {
|
|
28
|
+
paths.push(...collectSlugFieldPaths(f.fields, full));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return paths;
|
|
32
|
+
}
|
|
33
|
+
/** Traverse dot-path on a plain object; returns undefined if any segment missing. */
|
|
34
|
+
function readDotPath(data, path) {
|
|
35
|
+
const parts = path.split('.');
|
|
36
|
+
let current = data;
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (current == null || typeof current !== 'object')
|
|
39
|
+
return undefined;
|
|
40
|
+
current = current[part];
|
|
41
|
+
}
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
/** Resolve a slug value at dot-path; handles raw string or `{ lang: string }`. */
|
|
45
|
+
function readSlugAtPath(data, path, lang) {
|
|
46
|
+
const raw = readDotPath(data, path);
|
|
47
|
+
if (typeof raw === 'string')
|
|
48
|
+
return raw;
|
|
49
|
+
if (raw != null && typeof raw === 'object') {
|
|
50
|
+
return raw[lang];
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reject publishing when any slug-typed field (or `seo.slug`) collides with
|
|
56
|
+
* another entry's currently-published-or-scheduled version in the same
|
|
57
|
+
* collection + language. Each slug path is checked in its own namespace.
|
|
58
|
+
* Drafts, archived entries, the entry's own history, and other languages
|
|
59
|
+
* are ignored. Empty slug values are skipped — uniqueness is only
|
|
60
|
+
* meaningful for non-empty strings.
|
|
61
|
+
*
|
|
62
|
+
* @throws CmsError SLUG_DUPLICATE on the first conflict (`details.path` says
|
|
63
|
+
* which slug-field collided).
|
|
64
|
+
*/
|
|
65
|
+
export async function assertSlugUnique({ collection, lang, data, excludeEntryId }) {
|
|
66
|
+
const cms = getCMS();
|
|
67
|
+
let config;
|
|
68
|
+
try {
|
|
69
|
+
config = cms.getBySlug(collection);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return; // unknown collection — nothing to check
|
|
73
|
+
}
|
|
74
|
+
const fields = getFieldsFromConfig(config);
|
|
75
|
+
const slugPaths = collectSlugFieldPaths(fields);
|
|
76
|
+
// Snapshot incoming values, dropping empty entries up-front so we don't
|
|
77
|
+
// fetch other entries' data when nothing could collide.
|
|
78
|
+
const incoming = slugPaths
|
|
79
|
+
.map((path) => ({ path, value: readSlugAtPath(data, path, lang) }))
|
|
80
|
+
.filter((p) => typeof p.value === 'string' && p.value.trim() !== '');
|
|
81
|
+
if (incoming.length === 0)
|
|
82
|
+
return;
|
|
83
|
+
const dbEntries = await cms.databaseAdapter.getEntries({ slug: collection });
|
|
84
|
+
const otherIds = dbEntries
|
|
85
|
+
.filter((e) => e.archivedAt == null && e.id !== excludeEntryId)
|
|
86
|
+
.map((e) => e.id);
|
|
87
|
+
if (otherIds.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
const versions = await cms.databaseAdapter.getEntryVersions({
|
|
90
|
+
entryIds: otherIds,
|
|
91
|
+
lang
|
|
92
|
+
});
|
|
93
|
+
// Each other entry can have many historical versions — only its latest
|
|
94
|
+
// published-or-scheduled version is "live" for slug purposes, so collapse
|
|
95
|
+
// per entry before comparing.
|
|
96
|
+
const latestPerEntry = new Map();
|
|
97
|
+
for (const v of versions) {
|
|
98
|
+
if (v.publishedAt == null)
|
|
99
|
+
continue;
|
|
100
|
+
const cur = latestPerEntry.get(v.entryId);
|
|
101
|
+
if (!cur || v.versionNumber > cur.versionNumber) {
|
|
102
|
+
latestPerEntry.set(v.entryId, {
|
|
103
|
+
versionNumber: v.versionNumber,
|
|
104
|
+
data: v.data
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const [entryId, version] of latestPerEntry) {
|
|
109
|
+
for (const { path, value } of incoming) {
|
|
110
|
+
const otherSlug = readSlugAtPath(version.data, path, lang);
|
|
111
|
+
if (otherSlug === value) {
|
|
112
|
+
throw new CmsError('SLUG_DUPLICATE', `Slug „${value}" jest już używany w innym wpisie tej kolekcji (pole „${path}", język: ${lang}).`, { collection, lang, path, slug: value, conflictEntryId: entryId });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -11,7 +11,7 @@ export declare const updateEntryVersionSchema: z.ZodObject<{
|
|
|
11
11
|
publishedBy: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
12
12
|
}, z.z.core.$strip>;
|
|
13
13
|
export declare const updateEntryVersion: (id: string, data: Partial<DbEntryVersion>) => Promise<DbEntryVersion>;
|
|
14
|
-
export declare const updateEntryVersionCommandTypes: readonly ["draft", "published-now", "published-scheduled", "cancel-published"];
|
|
14
|
+
export declare const updateEntryVersionCommandTypes: readonly ["draft", "published-now", "published-scheduled", "cancel-published", "cancel-scheduled"];
|
|
15
15
|
export type UpdateEntryVersionCommandType = (typeof updateEntryVersionCommandTypes)[number];
|
|
16
16
|
/** Prune old draft versions scoped to (entryId, lang) */
|
|
17
17
|
export declare const pruneOldDraftVersions: (entryId: string, lang: string) => Promise<void>;
|
|
@@ -21,3 +21,8 @@ export declare const upsertDraftVersion: (entryId: string, data: Record<string,
|
|
|
21
21
|
}) => Promise<DbEntryVersion>;
|
|
22
22
|
/** Unpublish a specific language version for an entry */
|
|
23
23
|
export declare const unpublishEntryLang: (entryId: string, lang: string) => Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Cancel only the scheduled (future-dated) versions for a language, leaving the
|
|
26
|
+
* currently-live published version (publishedAt <= now) untouched.
|
|
27
|
+
*/
|
|
28
|
+
export declare const cancelScheduledEntryLang: (entryId: string, lang: string) => Promise<void>;
|
|
@@ -80,7 +80,8 @@ export const updateEntryVersionCommandTypes = [
|
|
|
80
80
|
'draft',
|
|
81
81
|
'published-now',
|
|
82
82
|
'published-scheduled',
|
|
83
|
-
'cancel-published'
|
|
83
|
+
'cancel-published',
|
|
84
|
+
'cancel-scheduled'
|
|
84
85
|
];
|
|
85
86
|
const MAX_DRAFT_VERSIONS = 10;
|
|
86
87
|
/** Prune old draft versions scoped to (entryId, lang) */
|
|
@@ -142,3 +143,25 @@ export const unpublishEntryLang = async (entryId, lang) => {
|
|
|
142
143
|
});
|
|
143
144
|
}));
|
|
144
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Cancel only the scheduled (future-dated) versions for a language, leaving the
|
|
148
|
+
* currently-live published version (publishedAt <= now) untouched.
|
|
149
|
+
*/
|
|
150
|
+
export const cancelScheduledEntryLang = async (entryId, lang) => {
|
|
151
|
+
const now = new Date();
|
|
152
|
+
const versions = await getCMS().databaseAdapter.getEntryVersions({
|
|
153
|
+
entryIds: [entryId],
|
|
154
|
+
lang
|
|
155
|
+
});
|
|
156
|
+
await Promise.all(versions
|
|
157
|
+
.filter((v) => v.publishedAt !== null && new Date(v.publishedAt) > now)
|
|
158
|
+
.map((version) => {
|
|
159
|
+
return getCMS().databaseAdapter.updateEntryVersion({
|
|
160
|
+
id: version.id,
|
|
161
|
+
data: {
|
|
162
|
+
publishedAt: null,
|
|
163
|
+
publishedBy: null
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}));
|
|
167
|
+
};
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Falls back to 'seo.slug' if no slugField is configured.
|
|
4
|
-
*/
|
|
1
|
+
export { getSlugFromEntryData } from '../../fields/slugPath.js';
|
|
2
|
+
/** Get dot-path to slug field for a collection. Falls back to 'seo.slug'. */
|
|
5
3
|
export declare function getEntrySlugPath(configSlug: string): string;
|
|
6
|
-
/**
|
|
7
|
-
* Traverse a dot-path in entry data and return the slug value.
|
|
8
|
-
* Handles both plain strings and localized objects { lang: string }.
|
|
9
|
-
*/
|
|
10
|
-
export declare function getSlugFromEntryData(data: Record<string, unknown>, slugPath: string, language?: string): string | undefined;
|
|
11
|
-
/**
|
|
12
|
-
* Build full entry path using collection's pathTemplate.
|
|
13
|
-
* If no template configured, returns the raw slug.
|
|
14
|
-
*/
|
|
4
|
+
/** Build full entry path using collection's pathTemplate. */
|
|
15
5
|
export declare function getEntryPath(configSlug: string, slug: string): string;
|