@michaelthielemann/kestrel 4.1.3 → 5.0.2
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 +284 -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 +162 -0
- package/dist/defineConfig.js +42 -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/allowlist.d.ts +9 -0
- package/dist/triggers/allowlist.js +49 -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 +59 -0
- package/dist/triggers/http.js +300 -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,68 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
definePageMeta({ layout: 'admin' })
|
|
3
|
-
|
|
4
|
-
const { t } = useT()
|
|
5
|
-
const route = useRoute()
|
|
6
|
-
const password = ref('')
|
|
7
|
-
const error = ref<string | null>(null)
|
|
8
|
-
const loading = ref(false)
|
|
9
|
-
|
|
10
|
-
async function submit() {
|
|
11
|
-
loading.value = true
|
|
12
|
-
error.value = null
|
|
13
|
-
try {
|
|
14
|
-
await useAuth().login(password.value)
|
|
15
|
-
await navigateTo(safeRedirect(route.query.redirect) ?? '/admin')
|
|
16
|
-
} catch (e: unknown) {
|
|
17
|
-
const status = (e as { statusCode?: number })?.statusCode
|
|
18
|
-
error.value = status === 401 ? t('login.invalid') : t('login.error')
|
|
19
|
-
} finally {
|
|
20
|
-
loading.value = false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<template>
|
|
26
|
-
<form class="login" @submit.prevent="submit">
|
|
27
|
-
<div class="login__brand">
|
|
28
|
-
<KestrelUiBrand />
|
|
29
|
-
<span class="login__brand-word">kestrel</span>
|
|
30
|
-
</div>
|
|
31
|
-
<h1 class="login__title">{{ t('login.signIn') }}</h1>
|
|
32
|
-
<KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
|
|
33
|
-
<KestrelUiField :label="t('login.password')">
|
|
34
|
-
<template #default="f">
|
|
35
|
-
<KestrelUiTextInput
|
|
36
|
-
v-model="password"
|
|
37
|
-
type="password"
|
|
38
|
-
autocomplete="current-password"
|
|
39
|
-
v-bind="f"
|
|
40
|
-
/>
|
|
41
|
-
</template>
|
|
42
|
-
</KestrelUiField>
|
|
43
|
-
<KestrelUiButton type="submit" variant="primary" :loading="loading">{{ t('login.signIn') }}</KestrelUiButton>
|
|
44
|
-
</form>
|
|
45
|
-
</template>
|
|
46
|
-
|
|
47
|
-
<style lang="scss">
|
|
48
|
-
.login {
|
|
49
|
-
display: flex;
|
|
50
|
-
flex-direction: column;
|
|
51
|
-
gap: var(--space-4);
|
|
52
|
-
max-width: 22rem;
|
|
53
|
-
margin-inline: auto;
|
|
54
|
-
overflow-y: auto;
|
|
55
|
-
|
|
56
|
-
&__brand {
|
|
57
|
-
display: inline-flex;
|
|
58
|
-
align-items: center;
|
|
59
|
-
gap: var(--space-2);
|
|
60
|
-
font-size: var(--text-lg);
|
|
61
|
-
font-weight: var(--weight-bold);
|
|
62
|
-
}
|
|
63
|
-
&__title {
|
|
64
|
-
font-size: var(--text-xl);
|
|
65
|
-
font-weight: var(--weight-bold);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
</style>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
|
|
3
|
-
const { t } = useT()
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<template>
|
|
7
|
-
<section class="media-page">
|
|
8
|
-
<h1 class="media-page__title">{{ t('media.title') }}</h1>
|
|
9
|
-
<KestrelMediaLibrary />
|
|
10
|
-
</section>
|
|
11
|
-
</template>
|
|
12
|
-
|
|
13
|
-
<style lang="scss">
|
|
14
|
-
// Non-scrolling page section: the title stays fixed and MediaLibrary's grid owns the scroll.
|
|
15
|
-
.media-page {
|
|
16
|
-
display: flex;
|
|
17
|
-
flex-direction: column;
|
|
18
|
-
gap: var(--space-4);
|
|
19
|
-
min-height: 0;
|
|
20
|
-
overflow: hidden;
|
|
21
|
-
|
|
22
|
-
&__title {
|
|
23
|
-
flex: 0 0 auto;
|
|
24
|
-
font-size: var(--text-xl);
|
|
25
|
-
font-weight: var(--weight-bold);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
</style>
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
definePageMeta({ layout: 'admin', middleware: 'admin-auth' })
|
|
3
|
-
|
|
4
|
-
// The global broken-references report: every record that links to a deleted or unpublished target. The
|
|
5
|
-
// referrer keeps its last published output (the stale link stays live) until it is re-edited — this is
|
|
6
|
-
// the editor's queue for fixing those. Derived on read, so an entry clears once the link is fixed or the
|
|
7
|
-
// target restored. Admin-only (the `brokenRefs` pipeline is outside the public set).
|
|
8
|
-
interface BrokenRow {
|
|
9
|
-
source: { collection: string; id: number }
|
|
10
|
-
target: { collection: string; id: number }
|
|
11
|
-
reason: 'missing' | 'unpublished'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { t } = useT()
|
|
15
|
-
const broken = ref<BrokenRow[]>([])
|
|
16
|
-
// Distinguish "the check ran and found nothing" from "the check failed": a failed fetch must NOT render
|
|
17
|
-
// the green all-clear (that would imply a verified-clean site when nothing was actually verified).
|
|
18
|
-
const loadError = ref(false)
|
|
19
|
-
try {
|
|
20
|
-
broken.value = await $fetch<BrokenRow[]>('/api/brokenRefs')
|
|
21
|
-
} catch {
|
|
22
|
-
loadError.value = true
|
|
23
|
-
}
|
|
24
|
-
</script>
|
|
25
|
-
|
|
26
|
-
<template>
|
|
27
|
-
<section class="refs">
|
|
28
|
-
<NuxtLink to="/admin" class="refs__back">
|
|
29
|
-
<KestrelUiIcon name="arrow-left" :size="16" />
|
|
30
|
-
<span>{{ t('nav.dashboard') }}</span>
|
|
31
|
-
</NuxtLink>
|
|
32
|
-
<h1 class="refs__title">{{ t('refs.title') }}</h1>
|
|
33
|
-
<p class="refs__lede">{{ t('refs.lede') }}</p>
|
|
34
|
-
|
|
35
|
-
<KestrelUiAlert v-if="loadError" variant="error" class="refs__error">{{ t('refs.loadError') }}</KestrelUiAlert>
|
|
36
|
-
<KestrelUiEmptyState
|
|
37
|
-
v-else-if="!broken.length"
|
|
38
|
-
icon="check"
|
|
39
|
-
:title="t('refs.empty.title')"
|
|
40
|
-
:description="t('refs.empty.desc')"
|
|
41
|
-
/>
|
|
42
|
-
<!-- The report table is the one scroll region; the back link, title and lede stay fixed above it. -->
|
|
43
|
-
<div v-else class="refs__scroll">
|
|
44
|
-
<table class="refs__table">
|
|
45
|
-
<thead>
|
|
46
|
-
<tr>
|
|
47
|
-
<th>{{ t('refs.colReferrer') }}</th>
|
|
48
|
-
<th>{{ t('refs.colTarget') }}</th>
|
|
49
|
-
<th>{{ t('refs.colReason') }}</th>
|
|
50
|
-
</tr>
|
|
51
|
-
</thead>
|
|
52
|
-
<tbody>
|
|
53
|
-
<tr v-for="(b, i) in broken" :key="i">
|
|
54
|
-
<td>
|
|
55
|
-
<NuxtLink :to="`/admin/${b.source.collection}/${b.source.id}`" class="refs__link">{{ b.source.collection }} #{{ b.source.id }}</NuxtLink>
|
|
56
|
-
</td>
|
|
57
|
-
<td class="refs__target">{{ b.target.collection }} #{{ b.target.id }}</td>
|
|
58
|
-
<td>
|
|
59
|
-
<span class="refs__reason" :class="`refs__reason--${b.reason}`">{{ t(`refs.reason.${b.reason}`) }}</span>
|
|
60
|
-
</td>
|
|
61
|
-
</tr>
|
|
62
|
-
</tbody>
|
|
63
|
-
</table>
|
|
64
|
-
</div>
|
|
65
|
-
</section>
|
|
66
|
-
</template>
|
|
67
|
-
|
|
68
|
-
<style lang="scss">
|
|
69
|
-
// Non-scrolling section: the back link, title and lede stay fixed; the report table scrolls in .refs__scroll.
|
|
70
|
-
.refs {
|
|
71
|
-
display: flex;
|
|
72
|
-
flex-direction: column;
|
|
73
|
-
gap: var(--space-3);
|
|
74
|
-
min-height: 0;
|
|
75
|
-
overflow: hidden;
|
|
76
|
-
|
|
77
|
-
&__back,
|
|
78
|
-
&__title,
|
|
79
|
-
&__lede {
|
|
80
|
-
flex: 0 0 auto;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
&__scroll {
|
|
84
|
-
flex: 1 1 auto;
|
|
85
|
-
min-height: 0;
|
|
86
|
-
overflow: auto;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
&__back {
|
|
90
|
-
display: inline-flex;
|
|
91
|
-
align-items: center;
|
|
92
|
-
gap: var(--space-1);
|
|
93
|
-
align-self: flex-start;
|
|
94
|
-
font-size: var(--text-sm);
|
|
95
|
-
color: var(--color-text-muted);
|
|
96
|
-
text-decoration: none;
|
|
97
|
-
|
|
98
|
-
&:hover {
|
|
99
|
-
color: var(--color-text);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
&__title {
|
|
103
|
-
font-size: var(--text-xl);
|
|
104
|
-
font-weight: var(--weight-bold);
|
|
105
|
-
}
|
|
106
|
-
&__lede {
|
|
107
|
-
color: var(--color-text-muted);
|
|
108
|
-
}
|
|
109
|
-
&__table {
|
|
110
|
-
width: 100%;
|
|
111
|
-
border-collapse: collapse;
|
|
112
|
-
font-size: var(--text-sm);
|
|
113
|
-
|
|
114
|
-
th,
|
|
115
|
-
td {
|
|
116
|
-
text-align: start;
|
|
117
|
-
padding: var(--space-2) var(--space-3);
|
|
118
|
-
border-bottom: 1px solid var(--color-border);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
&__link {
|
|
122
|
-
color: var(--color-primary);
|
|
123
|
-
text-decoration: none;
|
|
124
|
-
text-transform: capitalize;
|
|
125
|
-
}
|
|
126
|
-
&__target {
|
|
127
|
-
color: var(--color-text-muted);
|
|
128
|
-
text-transform: capitalize;
|
|
129
|
-
}
|
|
130
|
-
&__reason {
|
|
131
|
-
display: inline-flex;
|
|
132
|
-
padding: 1px var(--space-2);
|
|
133
|
-
border-radius: var(--radius-sm);
|
|
134
|
-
font-size: var(--text-xs);
|
|
135
|
-
font-weight: var(--weight-medium);
|
|
136
|
-
background: transparent;
|
|
137
|
-
border: 1px solid var(--color-warning);
|
|
138
|
-
color: var(--color-warning-text); /* warning conveyed as TEXT → needs 4.5:1, not the 3:1 icon accent */
|
|
139
|
-
|
|
140
|
-
&--missing {
|
|
141
|
-
background: var(--color-danger-solid);
|
|
142
|
-
color: var(--color-on-danger);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
</style>
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { isProtectedStyle } from '../utils/admin-style-guard'
|
|
2
|
-
|
|
3
|
-
function isKestrelOwned(node: Element): boolean {
|
|
4
|
-
const el = node as HTMLElement
|
|
5
|
-
const devId = el.dataset?.viteDevId ?? '' // Vite stamps the source path here in dev
|
|
6
|
-
const href = node.getAttribute?.('href') ?? ''
|
|
7
|
-
return isProtectedStyle(devId, href, el.dataset?.kestrel !== undefined)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Dev-only admin style guard — keeps a consumer's public CSS out of /admin.
|
|
12
|
-
*
|
|
13
|
-
* In `pnpm dev` the whole app (the consumer's public site AND /admin) is ONE Vite SPA sharing one
|
|
14
|
-
* `<head>`. A consumer's public global CSS — typically a reset with bare `html { font-size: … }`, `*`,
|
|
15
|
-
* `#__nuxt > div`, `body` — is injected as a persistent `<style>` the moment a public route renders, and
|
|
16
|
-
* Vite/Nuxt never unload it on client-side navigation. Soft-navigating to /admin therefore leaves that
|
|
17
|
-
* sheet live, and because it is globally-selectored it restyles the admin shell (rescaled rems from a
|
|
18
|
-
* changed root font-size, a collapsed rail, wrong fonts). Layout-chunk code-splitting only governs WHEN a
|
|
19
|
-
* sheet is loaded, never WHICH elements it matches once it is present in a shared document — so it is not
|
|
20
|
-
* isolation. This puts the defense upstream so consumers never have to scope their own styles defensively.
|
|
21
|
-
*
|
|
22
|
-
* While the active route is under /admin, this guard disables every stylesheet that is NOT Kestrel's own
|
|
23
|
-
* by flipping its `media` to `not all` (remembering the previous value so it can be restored exactly on
|
|
24
|
-
* leave — reversible, so returning to a public route never leaves it unstyled), and it watches `<head>` so
|
|
25
|
-
* any sheet injected later (HMR, another layer) is disabled too. On leaving /admin it restores everything.
|
|
26
|
-
*
|
|
27
|
-
* Gated to development: in production the public site ships as a separate static `nuxt generate` build with
|
|
28
|
-
* no /admin routes, and the origin serves /admin as its own document — there is no shared `<head>` to guard.
|
|
29
|
-
* Client-only (the static renderer never runs client plugins). Note: Kestrel's admin also re-asserts the
|
|
30
|
-
* root font-size inline while its layout is mounted (see admin.vue), which defends the rem base even in a
|
|
31
|
-
* single-origin production deployment where this dev guard is inactive.
|
|
32
|
-
*/
|
|
33
|
-
export default defineNuxtPlugin(() => {
|
|
34
|
-
if (!import.meta.dev) return
|
|
35
|
-
|
|
36
|
-
// Foreign sheets we disabled → their previous `media` value (null = attribute was absent).
|
|
37
|
-
const disabled = new Map<Element, string | null>()
|
|
38
|
-
let observer: MutationObserver | undefined
|
|
39
|
-
|
|
40
|
-
const disableForeign = () => {
|
|
41
|
-
for (const node of document.querySelectorAll('style, link[rel~="stylesheet"]')) {
|
|
42
|
-
if (isKestrelOwned(node) || disabled.has(node)) continue
|
|
43
|
-
disabled.set(node, node.getAttribute('media'))
|
|
44
|
-
node.setAttribute('media', 'not all')
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const restoreAll = () => {
|
|
49
|
-
for (const [node, media] of disabled) {
|
|
50
|
-
if (media === null) node.removeAttribute('media')
|
|
51
|
-
else node.setAttribute('media', media)
|
|
52
|
-
}
|
|
53
|
-
disabled.clear()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Runs on the initial client navigation and every subsequent one. A named global middleware — the
|
|
57
|
-
// anonymous form cannot take `{ global: true }`.
|
|
58
|
-
addRouteMiddleware(
|
|
59
|
-
'kestrel-admin-style-guard',
|
|
60
|
-
(to) => {
|
|
61
|
-
const onAdmin = to.path === '/admin' || to.path.startsWith('/admin/')
|
|
62
|
-
if (!onAdmin) {
|
|
63
|
-
observer?.disconnect()
|
|
64
|
-
observer = undefined
|
|
65
|
-
restoreAll()
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
disableForeign()
|
|
69
|
-
if (!observer) {
|
|
70
|
-
observer = new MutationObserver(disableForeign)
|
|
71
|
-
observer.observe(document.head, { childList: true, subtree: true })
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
{ global: true },
|
|
75
|
-
)
|
|
76
|
-
})
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { makeReauthInterceptor } from '../utils/reauth'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Global re-auth interceptor. When a guarded `/api` call 401s mid-session (the cookie expired or was
|
|
5
|
-
* rotated server-side), an ofetch `onResponseError` hook clears the stale local auth and bounces to the
|
|
6
|
-
* login screen with a redirect back to the current page. Client-only: the static renderer never runs
|
|
7
|
-
* client plugins, so the prerender principal is unaffected; non-admin/public locations are filtered out
|
|
8
|
-
* in `reauthTarget`.
|
|
9
|
-
*/
|
|
10
|
-
export default defineNuxtPlugin(() => {
|
|
11
|
-
const auth = useAuth()
|
|
12
|
-
const router = useRouter()
|
|
13
|
-
const onResponseError = makeReauthInterceptor({
|
|
14
|
-
currentPath: () => router.currentRoute.value.fullPath,
|
|
15
|
-
reset: () => auth.reset(),
|
|
16
|
-
navigate: (to) => { navigateTo(to) },
|
|
17
|
-
})
|
|
18
|
-
// Wrap the auto-imported `$fetch` so every call (incl. useFetch/useAsyncData) runs through the hook.
|
|
19
|
-
globalThis.$fetch = globalThis.$fetch.create({ onResponseError }) as typeof globalThis.$fetch
|
|
20
|
-
})
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Kestrel's own stylesheets live under `…/kestrel/layers/…`; its extensions ship as `kestrel-*` packages
|
|
2
|
-
// (or a monorepo `extensions/` dir → still resolved through a `kestrel-*` name). A Vue `<style scoped>`
|
|
3
|
-
// block is component-scoped (its rules carry a `data-v-<hash>` attribute), so it can NEVER be the global
|
|
4
|
-
// public CSS this guard targets — keep it wherever it comes from, which is what extension AND consumer
|
|
5
|
-
// admin widgets (custom field types, registered editor bodies) rely on.
|
|
6
|
-
const KESTREL_OWNED = /[/\\]kestrel[/\\]layers[/\\]/
|
|
7
|
-
const KESTREL_EXTENSION = /[/\\]kestrel-[^/\\]+[/\\]/
|
|
8
|
-
const SCOPED = /[?&]scoped[=&]/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Whether a stylesheet must be PROTECTED from the dev admin style-guard (i.e. NOT disabled on /admin). True
|
|
12
|
-
* for Kestrel core/extension sheets, any component-scoped Vue style, or a node explicitly tagged
|
|
13
|
-
* `data-kestrel`. Only genuinely foreign global CSS (a consumer's unscoped public reset) falls through to
|
|
14
|
-
* false and gets disabled. Pure → unit-tested without a DOM.
|
|
15
|
-
*/
|
|
16
|
-
export function isProtectedStyle(devId: string, href: string, hasDataKestrel: boolean): boolean {
|
|
17
|
-
if (hasDataKestrel) return true
|
|
18
|
-
if (SCOPED.test(devId)) return true
|
|
19
|
-
return KESTREL_OWNED.test(devId) || KESTREL_OWNED.test(href) || KESTREL_EXTENSION.test(devId) || KESTREL_EXTENSION.test(href)
|
|
20
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import type { SerializedBlock } from '@michaelthielemann/kestrel-core'
|
|
2
|
-
import { reorder } from '../../../ui/app/utils/reorder'
|
|
3
|
-
import { initialValues } from './edit-form'
|
|
4
|
-
|
|
5
|
-
// Pure, immutable operations over a nested block tree (`content`), addressed by each block's stable
|
|
6
|
-
// `id` rather than a positional index — this is what lets a single shared selection + one fields pane
|
|
7
|
-
// edit a block at ANY depth: every op walks the whole tree by id and rebuilds only the path to it. No
|
|
8
|
-
// Vue here — fully unit-testable.
|
|
9
|
-
|
|
10
|
-
export interface BlockRow {
|
|
11
|
-
id: string
|
|
12
|
-
type: string
|
|
13
|
-
props: Record<string, unknown>
|
|
14
|
-
slots?: Record<string, unknown>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type GenId = () => string
|
|
18
|
-
|
|
19
|
-
/** Stable config + id-addressed operations for the tree pane, threaded through the recursion as one
|
|
20
|
-
* object. Lives here (not in the SFC) so the component, the editor shell, and tests can share the type. */
|
|
21
|
-
export interface BlockTreeCtx {
|
|
22
|
-
byName: Record<string, SerializedBlock>
|
|
23
|
-
allowedTypes: SerializedBlock[]
|
|
24
|
-
ops: {
|
|
25
|
-
select: (id: string | null) => void
|
|
26
|
-
add: (parentId: string | null, slotName: string | null, type: string) => void
|
|
27
|
-
remove: (id: string) => void
|
|
28
|
-
move: (id: string, dir: -1 | 1) => void
|
|
29
|
-
duplicate: (id: string) => void
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** A seeded blank block of `type`: fresh id + props defaulted from the type's field schema. */
|
|
34
|
-
export function blankBlock(type: string, schemas: Record<string, SerializedBlock>, genId: GenId): BlockRow {
|
|
35
|
-
return { id: genId(), type, props: initialValues(schemas[type]?.fields ?? {}) }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Deep-clone a block subtree with fresh ids at every level (duplicate never shares an id or a ref). */
|
|
39
|
-
export function cloneBlockTree(b: BlockRow, genId: GenId): BlockRow {
|
|
40
|
-
const copy: BlockRow = { ...b, id: genId(), props: JSON.parse(JSON.stringify(b.props ?? {})) }
|
|
41
|
-
if (b.slots) {
|
|
42
|
-
copy.slots = Object.fromEntries(
|
|
43
|
-
Object.entries(b.slots).map(([name, arr]) => [name, Array.isArray(arr) ? (arr as BlockRow[]).map((c) => cloneBlockTree(c, genId)) : arr]),
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
return copy
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface Found {
|
|
50
|
-
block: BlockRow
|
|
51
|
-
/** The array that directly contains the block. */
|
|
52
|
-
siblings: BlockRow[]
|
|
53
|
-
index: number
|
|
54
|
-
/** The id of the block whose slot contains this one, or null at the root level. */
|
|
55
|
-
parentId: string | null
|
|
56
|
-
/** The slot name this block lives in, or null at the root level. */
|
|
57
|
-
slotName: string | null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Locate a block anywhere in the tree by id (with its container + position), or null. */
|
|
61
|
-
export function findInTree(blocks: BlockRow[], id: string): Found | null {
|
|
62
|
-
function walk(arr: BlockRow[], parentId: string | null, slotName: string | null): Found | null {
|
|
63
|
-
for (let i = 0; i < arr.length; i++) {
|
|
64
|
-
const b = arr[i]!
|
|
65
|
-
if (b.id === id) return { block: b, siblings: arr, index: i, parentId, slotName }
|
|
66
|
-
if (b.slots) {
|
|
67
|
-
for (const [name, sub] of Object.entries(b.slots)) {
|
|
68
|
-
if (Array.isArray(sub)) {
|
|
69
|
-
const hit = walk(sub as BlockRow[], b.id, name)
|
|
70
|
-
if (hit) return hit
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
return walk(blocks, null, null)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Immutably transform the block with `id` (block → new block), rebuilding ONLY the path to it; every
|
|
81
|
-
* untouched block AND array keeps its reference. Returning the original `blocks` when nothing changed is
|
|
82
|
-
* what makes the recursive `next !== sub` a correct change-detector (a bare `.map` always allocates, so it
|
|
83
|
-
* would rebuild every slot-bearing block in the tree on any edit). */
|
|
84
|
-
function updateBlock(blocks: BlockRow[], id: string, fn: (b: BlockRow) => BlockRow): BlockRow[] {
|
|
85
|
-
let changed = false
|
|
86
|
-
const out = blocks.map((b) => {
|
|
87
|
-
if (b.id === id) { changed = true; return fn(b) }
|
|
88
|
-
if (!b.slots) return b
|
|
89
|
-
const mapped = withMappedSlots(b)
|
|
90
|
-
if (mapped !== b) changed = true
|
|
91
|
-
return mapped
|
|
92
|
-
})
|
|
93
|
-
return changed ? out : blocks
|
|
94
|
-
function withMappedSlots(b: BlockRow): BlockRow {
|
|
95
|
-
let changed = false
|
|
96
|
-
const slots: Record<string, unknown> = {}
|
|
97
|
-
for (const [name, sub] of Object.entries(b.slots!)) {
|
|
98
|
-
if (Array.isArray(sub)) {
|
|
99
|
-
const next = updateBlock(sub as BlockRow[], id, fn)
|
|
100
|
-
if (next !== sub) changed = true
|
|
101
|
-
slots[name] = next
|
|
102
|
-
} else slots[name] = sub
|
|
103
|
-
}
|
|
104
|
-
return changed ? { ...b, slots } : b
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Immutably transform the ARRAY that directly contains `id` (array+index → new array); every untouched
|
|
109
|
-
* block AND array keeps its reference (same same-ref-when-unchanged rule as `updateBlock`). */
|
|
110
|
-
function updateContaining(blocks: BlockRow[], id: string, fn: (arr: BlockRow[], index: number) => BlockRow[]): BlockRow[] {
|
|
111
|
-
const idx = blocks.findIndex((b) => b.id === id)
|
|
112
|
-
if (idx !== -1) return fn(blocks, idx)
|
|
113
|
-
let changed = false
|
|
114
|
-
const out = blocks.map((b) => {
|
|
115
|
-
if (!b.slots) return b
|
|
116
|
-
let slotChanged = false
|
|
117
|
-
const slots: Record<string, unknown> = {}
|
|
118
|
-
for (const [name, sub] of Object.entries(b.slots)) {
|
|
119
|
-
if (Array.isArray(sub)) {
|
|
120
|
-
const next = updateContaining(sub as BlockRow[], id, fn)
|
|
121
|
-
if (next !== sub) slotChanged = true
|
|
122
|
-
slots[name] = next
|
|
123
|
-
} else slots[name] = sub
|
|
124
|
-
}
|
|
125
|
-
if (!slotChanged) return b
|
|
126
|
-
changed = true
|
|
127
|
-
return { ...b, slots }
|
|
128
|
-
})
|
|
129
|
-
return changed ? out : blocks
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function updatePropById(blocks: BlockRow[], id: string, key: string, value: unknown): BlockRow[] {
|
|
133
|
-
return updateBlock(blocks, id, (b) => ({ ...b, props: { ...b.props, [key]: value } }))
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Remove the block with `id` from wherever it lives. */
|
|
137
|
-
export function removeById(blocks: BlockRow[], id: string): BlockRow[] {
|
|
138
|
-
return updateContaining(blocks, id, (arr, i) => arr.filter((_, j) => j !== i))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Where selection should move when the block with `id` is removed: previous sibling, else next, else
|
|
142
|
-
* the parent (null at the root). Computed against the tree BEFORE removal. */
|
|
143
|
-
export function removalRetarget(blocks: BlockRow[], id: string): string | null {
|
|
144
|
-
const f = findInTree(blocks, id)
|
|
145
|
-
if (!f) return null
|
|
146
|
-
if (f.index > 0) return f.siblings[f.index - 1]!.id
|
|
147
|
-
if (f.index < f.siblings.length - 1) return f.siblings[f.index + 1]!.id
|
|
148
|
-
return f.parentId
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Move the block with `id` one step within its container (`dir` -1 up / +1 down); clamps at the edges. */
|
|
152
|
-
export function moveById(blocks: BlockRow[], id: string, dir: -1 | 1): BlockRow[] {
|
|
153
|
-
return updateContaining(blocks, id, (arr, i) => {
|
|
154
|
-
const to = i + dir
|
|
155
|
-
if (to < 0 || to >= arr.length) return arr
|
|
156
|
-
return reorder(arr, i, to)
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Duplicate the block with `id` (fresh ids throughout) right after it; returns the new tree + new id. */
|
|
161
|
-
export function duplicateById(blocks: BlockRow[], id: string, genId: GenId): { tree: BlockRow[]; newId: string } {
|
|
162
|
-
let newId = ''
|
|
163
|
-
const tree = updateContaining(blocks, id, (arr, i) => {
|
|
164
|
-
const copy = cloneBlockTree(arr[i]!, genId)
|
|
165
|
-
newId = copy.id
|
|
166
|
-
const next = [...arr]
|
|
167
|
-
next.splice(i + 1, 0, copy)
|
|
168
|
-
return next
|
|
169
|
-
})
|
|
170
|
-
return { tree, newId }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Append a seeded block of `type` at the root (`parentId` null) or into a parent's slot; returns the
|
|
174
|
-
* new tree + the new block's id. */
|
|
175
|
-
export function addBlock(
|
|
176
|
-
blocks: BlockRow[],
|
|
177
|
-
parentId: string | null,
|
|
178
|
-
slotName: string | null,
|
|
179
|
-
type: string,
|
|
180
|
-
schemas: Record<string, SerializedBlock>,
|
|
181
|
-
genId: GenId,
|
|
182
|
-
): { tree: BlockRow[]; newId: string } {
|
|
183
|
-
const block = blankBlock(type, schemas, genId)
|
|
184
|
-
if (parentId === null || slotName === null) return { tree: [...blocks, block], newId: block.id }
|
|
185
|
-
const tree = updateBlock(blocks, parentId, (parent) => {
|
|
186
|
-
const cur = Array.isArray(parent.slots?.[slotName]) ? (parent.slots![slotName] as BlockRow[]) : []
|
|
187
|
-
return { ...parent, slots: { ...(parent.slots ?? {}), [slotName]: [...cur, block] } }
|
|
188
|
-
})
|
|
189
|
-
return { tree, newId: block.id }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Ids of every block that itself has an error OR has a descendant with one (ancestor roll-up), for
|
|
193
|
-
* tree badges. `directIds` are the blocks the error map keys directly. */
|
|
194
|
-
export function errorBearingIds(blocks: BlockRow[], directIds: Set<string>): Set<string> {
|
|
195
|
-
const out = new Set<string>()
|
|
196
|
-
function walk(arr: BlockRow[]): boolean {
|
|
197
|
-
let any = false
|
|
198
|
-
for (const b of arr) {
|
|
199
|
-
let descendantHasError = false
|
|
200
|
-
if (b.slots) {
|
|
201
|
-
for (const sub of Object.values(b.slots)) {
|
|
202
|
-
if (Array.isArray(sub) && walk(sub as BlockRow[])) descendantHasError = true
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (directIds.has(b.id) || descendantHasError) {
|
|
206
|
-
out.add(b.id)
|
|
207
|
-
any = true
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return any
|
|
211
|
-
}
|
|
212
|
-
walk(blocks)
|
|
213
|
-
return out
|
|
214
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// Wire shapes for the collection batch endpoint, shared by the ops composable and the delete dialog.
|
|
2
|
-
// Mirrors media/app/utils/ops.ts: one definition, no drift between the caller and the confirm UI.
|
|
3
|
-
import type { SerializedAction } from '@michaelthielemann/kestrel-core'
|
|
4
|
-
|
|
5
|
-
/** The four batch actions the command endpoint accepts (a row action is a bulk op with one id). */
|
|
6
|
-
export type BatchAction = 'delete' | 'publish' | 'unpublish' | 'duplicate'
|
|
7
|
-
|
|
8
|
-
/** The built-in actions the admin renders with its own dedicated presentation (Delete/Duplicate buttons,
|
|
9
|
-
* the Publish/Unpublish pair) — always present on the wire, but never re-rendered generically. */
|
|
10
|
-
const BUILTIN_ACTION_NAMES = new Set(['deleteMany', 'duplicate'])
|
|
11
|
-
|
|
12
|
-
/** The schema-driven actions beyond the built-ins — what a consumer's `definePipeline` adds. */
|
|
13
|
-
export function customActions(actions: SerializedAction[]): SerializedAction[] {
|
|
14
|
-
return actions.filter((a) => !BUILTIN_ACTION_NAMES.has(a.name))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Custom actions invokable from the bulk-selection bar. */
|
|
18
|
-
export function bulkCustomActions(actions: SerializedAction[]): SerializedAction[] {
|
|
19
|
-
return customActions(actions).filter((a) => a.kind === 'bulk' || a.kind === 'both')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Custom actions invokable from a single row. */
|
|
23
|
-
export function recordCustomActions(actions: SerializedAction[]): SerializedAction[] {
|
|
24
|
-
return customActions(actions).filter((a) => a.kind === 'record' || a.kind === 'both')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** What a batch pipeline answers with: `deleteMany`/`updateMany` a count plus the ids they touched,
|
|
28
|
-
* `duplicate` the created rows. */
|
|
29
|
-
export type BatchPipelineResult = { count: number, ids: number[] } | { id: number }[]
|
|
30
|
-
|
|
31
|
-
/** The batch outcome the list UI reports. For `duplicate`, `ids` are the CREATED rows. A custom action's
|
|
32
|
-
* `action` is its pipeline name — not one of the four built-ins. */
|
|
33
|
-
export interface BulkResult {
|
|
34
|
-
action: BatchAction | (string & {})
|
|
35
|
-
count: number
|
|
36
|
-
ids: number[]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** GET /api/{collection}/referrers?ids=1,2,3 response: id -> how many other records reference it.
|
|
40
|
-
* `checked` is false when the index lookup itself failed, in which case `counts` is empty because nothing
|
|
41
|
-
* could be read — NOT because nothing links to the selection. */
|
|
42
|
-
export interface ReferrerCounts {
|
|
43
|
-
counts: Record<string, number>
|
|
44
|
-
checked: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The pre-delete summary shown in the confirm dialog: how many rows, and how many of them other
|
|
48
|
-
* records still link to (aggregated from the referrer counts). */
|
|
49
|
-
export interface BatchDeleteReport {
|
|
50
|
-
/** How many rows are about to be deleted. */
|
|
51
|
-
count: number
|
|
52
|
-
/** How many of those rows are referenced by at least one other record. */
|
|
53
|
-
referencedCount: number
|
|
54
|
-
/** Per-id breakdown for the referenced rows (id + inbound referrer count), in input order. */
|
|
55
|
-
referenced: { id: number; referrers: number }[]
|
|
56
|
-
/** Whether the referrer lookup actually ran. `false` means the check FAILED (not "none found"), so
|
|
57
|
-
* the dialog must warn that inbound links are unverified rather than imply a safe delete. */
|
|
58
|
-
checked: boolean
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Fold the referrer-count map into a delete report — pure, so it is unit-testable without a network.
|
|
62
|
-
* `checked` is the lookup's own outcome and is carried through untouched: an empty `counts` is ambiguous
|
|
63
|
-
* on its own, and only this flag tells the dialog whether it may claim nothing links to the selection. */
|
|
64
|
-
export function buildDeleteReport(ids: number[], counts: Record<string, number>, checked: boolean): BatchDeleteReport {
|
|
65
|
-
const referenced = ids
|
|
66
|
-
.map((id) => ({ id, referrers: Number(counts[String(id)] ?? 0) }))
|
|
67
|
-
.filter((r) => r.referrers > 0)
|
|
68
|
-
return { count: ids.length, referencedCount: referenced.length, referenced, checked }
|
|
69
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Client-side shaping of the per-record dead-reference map (the `/dead-refs` endpoint payload) into the
|
|
2
|
-
// sets the editor renders: which block nodes carry a stale reference (fed through `errorBearingIds` for
|
|
3
|
-
// the tree roll-up, exactly like block errors) and which field keys are stale at a given location. Pure
|
|
4
|
-
// + Vue-free so it unit-tests directly (happy-dom can't render the teleported field widgets).
|
|
5
|
-
|
|
6
|
-
/** One stale reference a record holds, with its field/block location + why it is dead. Wire shape of the
|
|
7
|
-
* server's `LocatedDeadRef`; kept here so the admin client doesn't import the server (drizzle) module. */
|
|
8
|
-
export interface DeadRef {
|
|
9
|
-
field: string
|
|
10
|
-
blockId?: string
|
|
11
|
-
collection: string
|
|
12
|
-
id: number
|
|
13
|
-
reason: 'missing' | 'unpublished'
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Block-node ids that DIRECTLY hold a dead reference — feed into `errorBearingIds` for the tree roll-up. */
|
|
17
|
-
export function deadBlockIds(refs: DeadRef[]): Set<string> {
|
|
18
|
-
const out = new Set<string>()
|
|
19
|
-
for (const r of refs) if (r.blockId) out.add(r.blockId)
|
|
20
|
-
return out
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Field keys with a dead reference at a location: a block id, or `null` for the record/page root. */
|
|
24
|
-
export function deadFieldsAt(refs: DeadRef[], blockId: string | null): Set<string> {
|
|
25
|
-
const out = new Set<string>()
|
|
26
|
-
for (const r of refs) {
|
|
27
|
-
if (blockId === null ? !r.blockId : r.blockId === blockId) out.add(r.field)
|
|
28
|
-
}
|
|
29
|
-
return out
|
|
30
|
-
}
|