@michaelthielemann/kestrel 4.1.2 → 5.0.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 +26 -201
- package/dist/boot.d.ts +45 -0
- package/dist/boot.js +274 -0
- package/dist/cast.d.ts +3 -0
- package/dist/cast.js +6 -0
- package/dist/catalogue.d.ts +11 -0
- package/dist/catalogue.js +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +55 -0
- package/dist/context.d.ts +62 -0
- package/dist/context.js +61 -0
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.js +65 -0
- package/dist/defineConfig.d.ts +157 -0
- package/dist/defineConfig.js +41 -0
- package/dist/defineContract.d.ts +12 -0
- package/dist/defineContract.js +16 -0
- package/dist/defineModule.d.ts +75 -0
- package/dist/defineModule.js +9 -0
- package/dist/definePipeline.d.ts +7 -0
- package/dist/definePipeline.js +11 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +48 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/load.d.ts +7 -0
- package/dist/load.js +79 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +36 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +87 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +19 -0
- package/dist/runner.d.ts +29 -0
- package/dist/runner.js +133 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +142 -0
- package/dist/sort.d.ts +2 -0
- package/dist/sort.js +50 -0
- package/dist/testing/runPipeline.d.ts +9 -0
- package/dist/testing/runPipeline.js +22 -0
- package/dist/triggers/cron.d.ts +10 -0
- package/dist/triggers/cron.js +44 -0
- package/dist/triggers/cronMatch.d.ts +10 -0
- package/dist/triggers/cronMatch.js +68 -0
- package/dist/triggers/http.d.ts +58 -0
- package/dist/triggers/http.js +292 -0
- package/package.json +25 -151
- package/NOTICE +0 -12
- package/kestrel.config.ts +0 -28
- package/layers/access/nuxt.config.ts +0 -5
- package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
- package/layers/access/server/middleware/access-guard.ts +0 -32
- package/layers/access/server/types.d.ts +0 -7
- package/layers/admin/app/components/AdminAccount.vue +0 -212
- package/layers/admin/app/components/AdminNav.vue +0 -44
- package/layers/admin/app/components/BlockFields.vue +0 -67
- package/layers/admin/app/components/BlockPreview.vue +0 -413
- package/layers/admin/app/components/BlockTree.vue +0 -433
- package/layers/admin/app/components/BlocksBody.vue +0 -163
- package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
- package/layers/admin/app/components/CollectionEditor.vue +0 -324
- package/layers/admin/app/components/CollectionList.vue +0 -429
- package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
- package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
- package/layers/admin/app/components/CollectionListPager.vue +0 -70
- package/layers/admin/app/components/CollectionListTable.vue +0 -347
- package/layers/admin/app/components/EditorStatus.vue +0 -211
- package/layers/admin/app/components/EditorUnsupported.vue +0 -28
- package/layers/admin/app/components/FieldsBody.vue +0 -23
- package/layers/admin/app/components/LocaleBar.vue +0 -107
- package/layers/admin/app/components/PageFields.vue +0 -129
- package/layers/admin/app/components/PageFieldsPane.vue +0 -39
- package/layers/admin/app/components/SeoFields.vue +0 -210
- package/layers/admin/app/components/SingletonEditor.vue +0 -93
- package/layers/admin/app/composables/useAuth.ts +0 -68
- package/layers/admin/app/composables/useBlockTree.ts +0 -95
- package/layers/admin/app/composables/useBlocks.ts +0 -7
- package/layers/admin/app/composables/useCachedResource.ts +0 -35
- package/layers/admin/app/composables/useCollectionOps.ts +0 -102
- package/layers/admin/app/composables/useCollections.ts +0 -6
- package/layers/admin/app/composables/useContentLocales.ts +0 -23
- package/layers/admin/app/composables/useEditForm.ts +0 -402
- package/layers/admin/app/composables/useLinkResolver.ts +0 -40
- package/layers/admin/app/composables/useListBatchActions.ts +0 -78
- package/layers/admin/app/composables/useListColumns.ts +0 -53
- package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
- package/layers/admin/app/composables/useListRows.ts +0 -75
- package/layers/admin/app/composables/useListSelection.ts +0 -28
- package/layers/admin/app/composables/useListUrlState.ts +0 -53
- package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
- package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
- package/layers/admin/app/composables/usePublishStatus.ts +0 -107
- package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
- package/layers/admin/app/composables/useTheme.ts +0 -36
- package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
- package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
- package/layers/admin/app/layouts/admin.vue +0 -266
- package/layers/admin/app/middleware/admin-auth.ts +0 -4
- package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
- package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
- package/layers/admin/app/pages/admin/index.vue +0 -118
- package/layers/admin/app/pages/admin/login.vue +0 -68
- package/layers/admin/app/pages/admin/media.vue +0 -28
- package/layers/admin/app/pages/admin/references.vue +0 -146
- package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
- package/layers/admin/app/plugins/reauth.client.ts +0 -20
- package/layers/admin/app/utils/admin-style-guard.ts +0 -20
- package/layers/admin/app/utils/block-tree.ts +0 -214
- package/layers/admin/app/utils/collection-ops.ts +0 -69
- package/layers/admin/app/utils/dead-refs.ts +0 -30
- package/layers/admin/app/utils/edit-form.ts +0 -267
- package/layers/admin/app/utils/editor-expose.ts +0 -36
- package/layers/admin/app/utils/editor-form-context.ts +0 -82
- package/layers/admin/app/utils/editor-registry.ts +0 -20
- package/layers/admin/app/utils/field-keys.ts +0 -11
- package/layers/admin/app/utils/list-cell.ts +0 -46
- package/layers/admin/app/utils/list-columns.ts +0 -142
- package/layers/admin/app/utils/list-query.ts +0 -96
- package/layers/admin/app/utils/nav-active.ts +0 -9
- package/layers/admin/app/utils/populate-blocks.ts +0 -288
- package/layers/admin/app/utils/preview-channel.ts +0 -62
- package/layers/admin/app/utils/preview-viewport.ts +0 -80
- package/layers/admin/app/utils/reauth.ts +0 -67
- package/layers/admin/app/utils/record-title.ts +0 -15
- package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
- package/layers/admin/app/utils/safe-redirect.ts +0 -12
- package/layers/admin/nuxt.config.ts +0 -20
- package/layers/auth/nuxt.config.ts +0 -1
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
- package/layers/collections/nuxt.config.ts +0 -1
- package/layers/collections/server/plugins/01.register.ts +0 -29
- package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
- package/layers/core/app/composables/layouts.ts +0 -5
- package/layers/core/app/utils/field-factories.ts +0 -129
- package/layers/core/app/utils/layouts.ts +0 -38
- package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
- package/layers/core/modules/auto-discovery/index.ts +0 -112
- package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
- package/layers/core/modules/auto-discovery/scan.ts +0 -170
- package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
- package/layers/core/modules/component-namespace/index.ts +0 -44
- package/layers/core/modules/component-namespace/shared.ts +0 -24
- package/layers/core/modules/kestrel/app-shell.ts +0 -55
- package/layers/core/modules/kestrel/auto-imports.ts +0 -44
- package/layers/core/modules/kestrel/index.ts +0 -144
- package/layers/core/modules/plugin-order/index.ts +0 -69
- package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
- package/layers/core/nuxt.config.ts +0 -10
- package/layers/core/server/api/[...path].ts +0 -98
- package/layers/core/server/plugins/00.config.ts +0 -12
- package/layers/core/server/plugins/00.migrate.ts +0 -12
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
- package/layers/core/server/plugins/02.schema-sync.ts +0 -58
- package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
- package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
- package/layers/core/server/tasks/db/migrate-module.ts +0 -83
- package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
- package/layers/core/server/tasks/db/migrate.ts +0 -50
- package/layers/core/server/utils/server-config.ts +0 -81
- package/layers/fields/nuxt.config.ts +0 -1
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
- package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
- package/layers/media/app/components/KestrelImg.vue +0 -94
- package/layers/media/app/components/MediaConflictDialog.vue +0 -35
- package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
- package/layers/media/app/components/MediaGrid.vue +0 -113
- package/layers/media/app/components/MediaImage.vue +0 -70
- package/layers/media/app/components/MediaLibrary.vue +0 -456
- package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
- package/layers/media/app/components/MediaPathBar.vue +0 -163
- package/layers/media/app/components/MediaPicker.vue +0 -19
- package/layers/media/app/components/MediaRenameDialog.vue +0 -32
- package/layers/media/app/components/MediaTable.vue +0 -75
- package/layers/media/app/components/MediaThumb.vue +0 -25
- package/layers/media/app/components/MediaToolbar.vue +0 -90
- package/layers/media/app/components/MediaUploadDialog.vue +0 -87
- package/layers/media/app/components/MediaViewer.vue +0 -137
- package/layers/media/app/components/field/Media.vue +0 -263
- package/layers/media/app/composables/useBlurUp.ts +0 -55
- package/layers/media/app/composables/useMediaClipboard.ts +0 -14
- package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
- package/layers/media/app/composables/useMediaDnd.ts +0 -56
- package/layers/media/app/composables/useMediaLibrary.ts +0 -152
- package/layers/media/app/composables/useMediaOps.ts +0 -50
- package/layers/media/app/composables/useMediaRelocate.ts +0 -39
- package/layers/media/app/composables/useMediaResolver.ts +0 -50
- package/layers/media/app/composables/useMediaUpload.ts +0 -93
- package/layers/media/app/composables/useMediaVariant.ts +0 -34
- package/layers/media/app/plugins/field-media.client.ts +0 -6
- package/layers/media/app/utils/ai-disclosure.ts +0 -19
- package/layers/media/app/utils/dnd.ts +0 -69
- package/layers/media/app/utils/library.ts +0 -81
- package/layers/media/app/utils/ops.ts +0 -60
- package/layers/media/app/utils/picture-model.ts +0 -125
- package/layers/media/app/utils/thumbhash.ts +0 -19
- package/layers/media/app/utils/variant-name.ts +0 -20
- package/layers/media/nuxt.config.ts +0 -7
- package/layers/media/server/middleware/ondemand-variants.ts +0 -38
- package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
- package/layers/media/server/plugins/02.register-media.ts +0 -22
- package/layers/media/server/plugins/04.variant-capture.ts +0 -27
- package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
- package/layers/media/server/tasks/media/backfill.ts +0 -17
- package/layers/public/app/app.vue +0 -5
- package/layers/public/app/components/BlockRenderer.vue +0 -56
- package/layers/public/app/components/KestrelLink.vue +0 -26
- package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
- package/layers/public/app/composables/public-page.ts +0 -24
- package/layers/public/app/error.vue +0 -12
- package/layers/public/app/layouts/default.vue +0 -5
- package/layers/public/app/pages/[...slug].vue +0 -239
- package/layers/public/app/pages/__kestrel/preview.vue +0 -46
- package/layers/public/app/utils/block-edit-context.ts +0 -16
- package/layers/public/app/utils/json-ld.ts +0 -139
- package/layers/public/app/utils/link-href.ts +0 -46
- package/layers/public/app/utils/page-head.ts +0 -84
- package/layers/public/app/utils/page-layout.ts +0 -18
- package/layers/public/app/utils/preview-protocol.ts +0 -117
- package/layers/public/app/utils/public-route.ts +0 -46
- package/layers/public/app/utils/site-head.ts +0 -43
- package/layers/public/modules/deploy-output/index.ts +0 -80
- package/layers/public/modules/prerender-routes/discover.ts +0 -37
- package/layers/public/modules/prerender-routes/index.ts +0 -88
- package/layers/public/modules/prerender-routes/routes.ts +0 -16
- package/layers/public/modules/prune-media/index.ts +0 -48
- package/layers/public/modules/prune-media/prune-media.ts +0 -81
- package/layers/public/nuxt.config.ts +0 -14
- package/layers/public/server/delivery-live/serve.ts +0 -111
- package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
- package/layers/public/server/pipelines/index.ts +0 -23
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
- package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
- package/layers/public/server/plugins/02.register-links.ts +0 -16
- package/layers/public/server/plugins/03.redirects.ts +0 -55
- package/layers/public/server/plugins/05.plan-publish.ts +0 -5
- package/layers/public/server/plugins/zz.publish.ts +0 -127
- package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
- package/layers/public/server/routes/llms.txt.get.ts +0 -66
- package/layers/public/server/routes/redirects.json.get.ts +0 -58
- package/layers/public/server/routes/robots.txt.get.ts +0 -11
- package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
- package/layers/public/server/tasks/publish/run.ts +0 -32
- package/layers/public/server/utils/publish/render-live.ts +0 -26
- package/layers/ui/app/assets/scss/_base.scss +0 -16
- package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
- package/layers/ui/app/assets/scss/_mixins.scss +0 -50
- package/layers/ui/app/assets/scss/_reset.scss +0 -65
- package/layers/ui/app/assets/scss/_tokens.scss +0 -183
- package/layers/ui/app/assets/scss/main.scss +0 -4
- package/layers/ui/app/components/field/Boolean.vue +0 -30
- package/layers/ui/app/components/field/Choice.vue +0 -73
- package/layers/ui/app/components/field/Datetime.vue +0 -98
- package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
- package/layers/ui/app/components/field/Json.vue +0 -62
- package/layers/ui/app/components/field/Layout.vue +0 -138
- package/layers/ui/app/components/field/Link.vue +0 -112
- package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
- package/layers/ui/app/components/field/Number.vue +0 -30
- package/layers/ui/app/components/field/Relation.vue +0 -49
- package/layers/ui/app/components/field/Renderer.vue +0 -30
- package/layers/ui/app/components/field/Repeater.vue +0 -460
- package/layers/ui/app/components/field/Richtext.vue +0 -26
- package/layers/ui/app/components/field/Slug.vue +0 -65
- package/layers/ui/app/components/field/Text.vue +0 -35
- package/layers/ui/app/components/field/Unsupported.vue +0 -34
- package/layers/ui/app/components/ui/Alert.vue +0 -50
- package/layers/ui/app/components/ui/Brand.vue +0 -22
- package/layers/ui/app/components/ui/Button.vue +0 -116
- package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
- package/layers/ui/app/components/ui/Checkbox.vue +0 -53
- package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
- package/layers/ui/app/components/ui/Combobox.vue +0 -345
- package/layers/ui/app/components/ui/DatePicker.vue +0 -92
- package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
- package/layers/ui/app/components/ui/Dialog.vue +0 -107
- package/layers/ui/app/components/ui/EmptyState.vue +0 -46
- package/layers/ui/app/components/ui/Field.vue +0 -58
- package/layers/ui/app/components/ui/Fieldset.vue +0 -65
- package/layers/ui/app/components/ui/Icon.vue +0 -44
- package/layers/ui/app/components/ui/Menu.vue +0 -54
- package/layers/ui/app/components/ui/NumberInput.vue +0 -175
- package/layers/ui/app/components/ui/Richtext.vue +0 -141
- package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
- package/layers/ui/app/components/ui/Select.vue +0 -52
- package/layers/ui/app/components/ui/TextInput.vue +0 -111
- package/layers/ui/app/components/ui/Textarea.vue +0 -39
- package/layers/ui/app/components/ui/TimeInput.vue +0 -18
- package/layers/ui/app/components/ui/Toasts.vue +0 -103
- package/layers/ui/app/components/ui/Tooltip.vue +0 -45
- package/layers/ui/app/composables/useAdminLang.ts +0 -8
- package/layers/ui/app/composables/useDragReorder.ts +0 -41
- package/layers/ui/app/composables/useEchoGuard.ts +0 -28
- package/layers/ui/app/composables/useLinkField.ts +0 -70
- package/layers/ui/app/composables/useRecordOptions.ts +0 -84
- package/layers/ui/app/composables/useRepeater.ts +0 -99
- package/layers/ui/app/composables/useT.ts +0 -32
- package/layers/ui/app/composables/useToast.ts +0 -46
- package/layers/ui/app/i18n/de.ts +0 -432
- package/layers/ui/app/i18n/en.ts +0 -434
- package/layers/ui/app/utils/boolean-field.ts +0 -10
- package/layers/ui/app/utils/field-component.ts +0 -22
- package/layers/ui/app/utils/field-empty.ts +0 -21
- package/layers/ui/app/utils/field-registry.ts +0 -36
- package/layers/ui/app/utils/humanize.ts +0 -14
- package/layers/ui/app/utils/icons.ts +0 -143
- package/layers/ui/app/utils/iso-date.ts +0 -18
- package/layers/ui/app/utils/localized.ts +0 -13
- package/layers/ui/app/utils/record-context.ts +0 -15
- package/layers/ui/app/utils/reorder.ts +0 -8
- package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
- package/layers/ui/app/utils/useFieldA11y.ts +0 -40
- package/layers/ui/nuxt.config.ts +0 -9
- package/nuxt.config.ts +0 -108
- package/scripts/consumer-template-ci.mjs +0 -160
- package/scripts/copy-create-payload.mjs +0 -49
- package/scripts/coverage-all.mjs +0 -163
- package/scripts/dashboard.mjs +0 -89
- package/scripts/hash-password.mjs +0 -29
- package/scripts/kestrel.mjs +0 -240
- package/scripts/lib/cli.mjs +0 -129
- package/scripts/lib/coverage-model.mjs +0 -51
- package/scripts/lib/password.mjs +0 -19
- package/scripts/lib/scaffold.mjs +0 -205
- package/scripts/port-graph.mjs +0 -350
- package/scripts/typecheck.mjs +0 -62
- package/templates/starter/README.md +0 -65
- package/templates/starter/_env.example +0 -17
- package/templates/starter/_gitignore +0 -26
- package/templates/starter/_package.json +0 -22
- package/templates/starter/app/app.vue +0 -7
- package/templates/starter/app/blocks/Prose.vue +0 -13
- package/templates/starter/app/layouts/default.vue +0 -6
- package/templates/starter/nuxt.config.ts +0 -23
- package/templates/starter/pnpm-workspace.yaml +0 -8
- package/templates/starter/tsconfig.json +0 -3
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { ref, computed } from 'vue'
|
|
2
|
-
import type { Ref } from 'vue'
|
|
3
|
-
import { useEchoGuard } from '../../../ui/app/composables/useEchoGuard'
|
|
4
|
-
import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
|
|
5
|
-
import {
|
|
6
|
-
type BlockRow,
|
|
7
|
-
findInTree,
|
|
8
|
-
updatePropById,
|
|
9
|
-
removeById,
|
|
10
|
-
removalRetarget,
|
|
11
|
-
moveById,
|
|
12
|
-
duplicateById,
|
|
13
|
-
addBlock,
|
|
14
|
-
} from '../utils/block-tree'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Reactive state for the 3-pane block editor: one nested `content` tree plus a single shared selection
|
|
18
|
-
* (the stable `block.id`, or `null` for the page root). The tree pane and the fields pane both drive
|
|
19
|
-
* the same instance — the tree selects + does structural ops, the fields pane edits the selected
|
|
20
|
-
* block's props. Every mutation is id-addressed and immutable (see `block-tree.ts`) and re-emits the
|
|
21
|
-
* whole array (so `useEditForm`'s `setField('content', …)` reconciliation still runs). `genId` is
|
|
22
|
-
* injectable for deterministic tests; defaults to `crypto.randomUUID` (the server requires a non-empty id).
|
|
23
|
-
*/
|
|
24
|
-
export function useBlockTree(
|
|
25
|
-
model: Ref<unknown[] | null | undefined>,
|
|
26
|
-
schemas: Ref<Record<string, SerializedBlock>>,
|
|
27
|
-
genId: () => string = () => crypto.randomUUID(),
|
|
28
|
-
// Routes an emit through the caller's own undo-history capture point instead of a plain `model.value =`
|
|
29
|
-
// assignment, tagged with a per-op coalesce key (see `emit`) — so a structural op landing inside another
|
|
30
|
-
// edit's coalesce window (e.g. a delete right after typing) still gets its own undo step.
|
|
31
|
-
setContent?: (value: unknown[], coalesceAs: string) => void,
|
|
32
|
-
) {
|
|
33
|
-
// JSON round-trip (not structuredClone — which throws on Vue reactive proxies): reads through the
|
|
34
|
-
// proxy into detached plain objects. Block content is JSON by definition, so this is lossless here.
|
|
35
|
-
const clone = (v: unknown): BlockRow[] => (Array.isArray(v) ? (JSON.parse(JSON.stringify(v)) as BlockRow[]) : [])
|
|
36
|
-
|
|
37
|
-
const blocks = ref<BlockRow[]>(clone(model.value))
|
|
38
|
-
const selectedId = ref<string | null>(null)
|
|
39
|
-
|
|
40
|
-
function emit(coalesceAs: string): void {
|
|
41
|
-
if (setContent) setContent(blocks.value, coalesceAs)
|
|
42
|
-
else model.value = blocks.value
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Echo-guard against the tree's OWN emit (shared primitive): an external change (load / locale copy /
|
|
46
|
-
// undo / redo) differs from the current tree, so it reseeds and drops a now-missing selection to root.
|
|
47
|
-
useEchoGuard(model, () => blocks.value, (v) => {
|
|
48
|
-
blocks.value = clone(v)
|
|
49
|
-
if (selectedId.value !== null && !findInTree(blocks.value, selectedId.value)) selectedId.value = null
|
|
50
|
-
}, [])
|
|
51
|
-
|
|
52
|
-
const selectedBlock = computed<BlockRow | null>(() =>
|
|
53
|
-
selectedId.value === null ? null : (findInTree(blocks.value, selectedId.value)?.block ?? null),
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
function select(id: string | null): void {
|
|
57
|
-
selectedId.value = id
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function setProp(id: string, key: string, value: unknown): void {
|
|
61
|
-
blocks.value = updatePropById(blocks.value, id, key, value)
|
|
62
|
-
// Coalesces a typing burst into that one field, same as an ordinary text field would.
|
|
63
|
-
emit(`content:prop:${id}:${key}`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function remove(id: string): void {
|
|
67
|
-
const retarget = removalRetarget(blocks.value, id)
|
|
68
|
-
blocks.value = removeById(blocks.value, id)
|
|
69
|
-
// Heal the selection whenever it no longer exists — covers removing the selected block itself AND
|
|
70
|
-
// removing an ANCESTOR of it (the whole subtree goes); land on the removed block's sibling/parent.
|
|
71
|
-
if (selectedId.value !== null && !findInTree(blocks.value, selectedId.value)) selectedId.value = retarget
|
|
72
|
-
emit(`content:remove:${id}`)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function move(id: string, dir: -1 | 1): void {
|
|
76
|
-
blocks.value = moveById(blocks.value, id, dir)
|
|
77
|
-
emit(`content:move:${id}`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function duplicate(id: string): void {
|
|
81
|
-
const { tree, newId } = duplicateById(blocks.value, id, genId)
|
|
82
|
-
blocks.value = tree
|
|
83
|
-
selectedId.value = newId
|
|
84
|
-
emit(`content:duplicate:${id}`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function add(parentId: string | null, slotName: string | null, type: string): void {
|
|
88
|
-
const { tree, newId } = addBlock(blocks.value, parentId, slotName, type, schemas.value, genId)
|
|
89
|
-
blocks.value = tree
|
|
90
|
-
selectedId.value = newId
|
|
91
|
-
emit(`content:add:${parentId}:${slotName}:${type}`)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { blocks, selectedId, selectedBlock, select, setProp, remove, move, duplicate, add }
|
|
95
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
|
|
3
|
-
// Thin wrapper over the shared cached-resource loader; keeps the named `blocks` return.
|
|
4
|
-
export function useBlocks() {
|
|
5
|
-
const { state: blocks, load } = useCachedResource<SerializedBlock>('kestrel-blocks', '/api/blocks')
|
|
6
|
-
return { blocks, load }
|
|
7
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// Generic cached-resource loader: a `useState`-backed list that fetches once and is shared by every
|
|
2
|
-
// caller. Backs `useBlocks` / `useCollections` (structurally identical loaders).
|
|
3
|
-
//
|
|
4
|
-
// SSR isolation: the in-flight (dedup) promise is keyed first by the per-request Nuxt app (a WeakMap,
|
|
5
|
-
// so it can't leak across SSR requests) AND then by `stateKey` — otherwise two different resources
|
|
6
|
-
// loading concurrently in ONE request would share a single promise and cross-contaminate (one
|
|
7
|
-
// resolves with the other's data).
|
|
8
|
-
const inflight = new WeakMap<object, Map<string, Promise<unknown>>>()
|
|
9
|
-
|
|
10
|
-
export function useCachedResource<T>(stateKey: string, url: string) {
|
|
11
|
-
const state = useState<T[] | null>(stateKey, () => null)
|
|
12
|
-
const nuxtApp = useNuxtApp()
|
|
13
|
-
|
|
14
|
-
async function load(): Promise<T[]> {
|
|
15
|
-
if (state.value) return state.value
|
|
16
|
-
let perApp = inflight.get(nuxtApp)
|
|
17
|
-
if (!perApp) {
|
|
18
|
-
perApp = new Map()
|
|
19
|
-
inflight.set(nuxtApp, perApp)
|
|
20
|
-
}
|
|
21
|
-
let pending = perApp.get(stateKey) as Promise<T[]> | undefined
|
|
22
|
-
if (!pending) {
|
|
23
|
-
pending = $fetch<{ data: T[] }>(url)
|
|
24
|
-
.then(({ data }) => {
|
|
25
|
-
state.value = data
|
|
26
|
-
return data
|
|
27
|
-
})
|
|
28
|
-
.finally(() => perApp!.delete(stateKey))
|
|
29
|
-
perApp.set(stateKey, pending)
|
|
30
|
-
}
|
|
31
|
-
return pending
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return { state, load }
|
|
35
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { ref, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
-
import { buildDeleteReport, type BatchAction, type BatchDeleteReport, type BatchPipelineResult, type BulkResult, type ReferrerCounts } from '../utils/collection-ops'
|
|
3
|
-
import type { SerializedAction } from '@michaelthielemann/kestrel-core'
|
|
4
|
-
|
|
5
|
-
/** Which write pipeline each batch action runs, and the body it takes. A publish/unpublish is an
|
|
6
|
-
* `updateMany` with a status patch — the same pipeline any other field-wide edit would use. */
|
|
7
|
-
const BATCH_PIPELINE: Record<BatchAction, { op: string, patch?: { status: 'published' | 'draft' } }> = {
|
|
8
|
-
delete: { op: 'deleteMany' },
|
|
9
|
-
duplicate: { op: 'duplicate' },
|
|
10
|
-
publish: { op: 'updateMany', patch: { status: 'published' } },
|
|
11
|
-
unpublish: { op: 'updateMany', patch: { status: 'draft' } },
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Client orchestration for the collection batch operations — ONE module, two entry points: a row action
|
|
16
|
-
* passes a single id, the bulk bar passes many. Every mutating call runs a batch pipeline
|
|
17
|
-
* (`POST /api/{collection}/{pipeline}`); the server does the batching, the client never loops N requests.
|
|
18
|
-
* Mirrors media/useMediaOps (busy/error + an onChanged refetch hook).
|
|
19
|
-
*/
|
|
20
|
-
export function useCollectionOps(collection: MaybeRefOrGetter<string>, onChanged?: () => void | Promise<void>) {
|
|
21
|
-
const { t } = useT()
|
|
22
|
-
const busy = ref(false)
|
|
23
|
-
const error = ref<string | null>(null)
|
|
24
|
-
const name = () => toValue(collection)
|
|
25
|
-
|
|
26
|
-
async function bulk(action: BatchAction, ids: number[]): Promise<BulkResult> {
|
|
27
|
-
busy.value = true
|
|
28
|
-
error.value = null
|
|
29
|
-
try {
|
|
30
|
-
const { op, patch } = BATCH_PIPELINE[action]
|
|
31
|
-
const res = await $fetch<BatchPipelineResult>(`/api/${name()}/${op}`, { method: 'POST', body: { ids, ...(patch ? { patch } : {}) } })
|
|
32
|
-
await onChanged?.()
|
|
33
|
-
// `duplicate` answers with the CREATED rows, the two batch ops with a count + the ids they touched;
|
|
34
|
-
// both fold into the one shape the list UI reports.
|
|
35
|
-
return Array.isArray(res)
|
|
36
|
-
? { action, count: res.length, ids: res.map((row) => Number(row.id)) }
|
|
37
|
-
: { action, count: res.count, ids: res.ids }
|
|
38
|
-
} catch (e) {
|
|
39
|
-
error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.opFailed')
|
|
40
|
-
throw e
|
|
41
|
-
} finally {
|
|
42
|
-
busy.value = false
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Read-only pre-delete summary (referrer aggregate). Best-effort: sets `error` and rethrows on failure
|
|
47
|
-
* so the caller can decide whether to still proceed (deletion is not blocked on the warning lookup). */
|
|
48
|
-
async function previewDelete(ids: number[]): Promise<BatchDeleteReport> {
|
|
49
|
-
error.value = null
|
|
50
|
-
try {
|
|
51
|
-
const res = await $fetch<ReferrerCounts>(`/api/${name()}/referrers`, { query: { ids: ids.join(',') } })
|
|
52
|
-
// The endpoint flags whether the index lookup ran; carry that through rather than inferring safety from
|
|
53
|
-
// an empty `counts`. A missing body is indeterminate too — it is no evidence that nothing links here.
|
|
54
|
-
return buildDeleteReport(ids, res?.counts ?? {}, res != null && res.checked !== false)
|
|
55
|
-
} catch (e) {
|
|
56
|
-
error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.refCheckFailed')
|
|
57
|
-
throw e
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Run a schema-driven custom action (a consumer's `definePipeline` beyond the built-in bulk set). The
|
|
63
|
-
* response shape is whatever that pipeline answers with — unlike the built-ins, generic rendering has no
|
|
64
|
-
* contract to decode, so the reported result is simply the ids the caller asked to act on.
|
|
65
|
-
*/
|
|
66
|
-
async function runCustomAction(action: SerializedAction, ids: number[]): Promise<BulkResult> {
|
|
67
|
-
busy.value = true
|
|
68
|
-
error.value = null
|
|
69
|
-
try {
|
|
70
|
-
const record = action.kind === 'record'
|
|
71
|
-
const url = record ? `/api/${name()}/${action.name}/${ids[0]}` : `/api/${name()}/${action.name}`
|
|
72
|
-
await $fetch(url, { method: 'POST', body: record ? undefined : { ids } })
|
|
73
|
-
await onChanged?.()
|
|
74
|
-
return { action: action.name, count: ids.length, ids }
|
|
75
|
-
} catch (e) {
|
|
76
|
-
error.value = (e as { statusMessage?: string })?.statusMessage ?? t('list.opFailed')
|
|
77
|
-
throw e
|
|
78
|
-
} finally {
|
|
79
|
-
busy.value = false
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const confirmDelete = (ids: number[]) => bulk('delete', ids)
|
|
84
|
-
const duplicate = (ids: number[]) => bulk('duplicate', ids)
|
|
85
|
-
/**
|
|
86
|
-
* Persist a new status and, for a publish, write the static output too (ADR-0008). Two calls because
|
|
87
|
-
* they are two things: `updateMany` owns the DB write (validation, all-or-nothing, write events),
|
|
88
|
-
* `/api/publish` owns the render. Unpublishing needs no second call — the write event prunes the pages
|
|
89
|
-
* on its own, since taking a page offline must never wait for a separate action.
|
|
90
|
-
*/
|
|
91
|
-
const setStatus = async (ids: number[], status: 'published' | 'draft'): Promise<BulkResult> => {
|
|
92
|
-
const res = await bulk(status === 'published' ? 'publish' : 'unpublish', ids)
|
|
93
|
-
if (status === 'published') {
|
|
94
|
-
// Best-effort: the records ARE published (the state the list shows); a failed render surfaces on the
|
|
95
|
-
// record's own status lamp, and re-pressing Publish retries it.
|
|
96
|
-
await $fetch('/api/publish', { method: 'POST', body: { collection: name(), ids } }).catch(() => null)
|
|
97
|
-
}
|
|
98
|
-
return res
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return { busy, error, previewDelete, confirmDelete, duplicate, setStatus, runCustomAction }
|
|
102
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { SerializedCollection } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
|
|
3
|
-
export function useCollections() {
|
|
4
|
-
const { state: collections, load } = useCachedResource<SerializedCollection>('kestrel-collections', '/api/collections')
|
|
5
|
-
return { collections, load }
|
|
6
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface ContentLocales {
|
|
2
|
-
/** Website/content locales (NOT the admin-UI language) — what the editor LocaleBar offers. */
|
|
3
|
-
locales: string[]
|
|
4
|
-
primary: string
|
|
5
|
-
/** Whether the primary locale is prefixed in public URLs too (`/en/about`); the `localePath` flag. */
|
|
6
|
-
prefixPrimary: boolean
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Resolve the configured content locales from a `runtimeConfig.public`-shaped object. Pure, so the
|
|
10
|
-
* fallback/primary logic is unit-testable without a Nuxt context. The `['en']` fallback is a safe
|
|
11
|
-
* last resort for an absent/empty config (nuxt.config always sets `public.locales`): primary-only is
|
|
12
|
-
* a safer guess than assuming a second locale exists, so it intentionally differs from the server's
|
|
13
|
-
* `['en','de']` *default* (which applies only when KESTREL_LOCALES itself is unset). */
|
|
14
|
-
export function pickContentLocales(pub: { locales?: unknown; primaryLocale?: unknown; prefixPrimary?: unknown }): ContentLocales {
|
|
15
|
-
const locales = Array.isArray(pub.locales) && pub.locales.length ? pub.locales.map(String) : ['en']
|
|
16
|
-
const primary =
|
|
17
|
-
typeof pub.primaryLocale === 'string' && locales.includes(pub.primaryLocale) ? pub.primaryLocale : locales[0]!
|
|
18
|
-
return { locales, primary, prefixPrimary: pub.prefixPrimary === true }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function useContentLocales(): ContentLocales {
|
|
22
|
-
return pickContentLocales(useRuntimeConfig().public as { locales?: unknown; primaryLocale?: unknown; prefixPrimary?: unknown })
|
|
23
|
-
}
|