@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.
Files changed (340) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +274 -0
  4. package/dist/cast.d.ts +3 -0
  5. package/dist/cast.js +6 -0
  6. package/dist/catalogue.d.ts +11 -0
  7. package/dist/catalogue.js +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +55 -0
  10. package/dist/context.d.ts +62 -0
  11. package/dist/context.js +61 -0
  12. package/dist/dataflow.d.ts +3 -0
  13. package/dist/dataflow.js +65 -0
  14. package/dist/defineConfig.d.ts +157 -0
  15. package/dist/defineConfig.js +41 -0
  16. package/dist/defineContract.d.ts +12 -0
  17. package/dist/defineContract.js +16 -0
  18. package/dist/defineModule.d.ts +75 -0
  19. package/dist/defineModule.js +9 -0
  20. package/dist/definePipeline.d.ts +7 -0
  21. package/dist/definePipeline.js +11 -0
  22. package/dist/errors.d.ts +26 -0
  23. package/dist/errors.js +48 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.js +14 -0
  26. package/dist/load.d.ts +7 -0
  27. package/dist/load.js +79 -0
  28. package/dist/logger.d.ts +18 -0
  29. package/dist/logger.js +36 -0
  30. package/dist/registry.d.ts +14 -0
  31. package/dist/registry.js +87 -0
  32. package/dist/result.d.ts +19 -0
  33. package/dist/result.js +19 -0
  34. package/dist/runner.d.ts +29 -0
  35. package/dist/runner.js +133 -0
  36. package/dist/schema.d.ts +7 -0
  37. package/dist/schema.js +142 -0
  38. package/dist/sort.d.ts +2 -0
  39. package/dist/sort.js +50 -0
  40. package/dist/testing/runPipeline.d.ts +9 -0
  41. package/dist/testing/runPipeline.js +22 -0
  42. package/dist/triggers/cron.d.ts +10 -0
  43. package/dist/triggers/cron.js +44 -0
  44. package/dist/triggers/cronMatch.d.ts +10 -0
  45. package/dist/triggers/cronMatch.js +68 -0
  46. package/dist/triggers/http.d.ts +58 -0
  47. package/dist/triggers/http.js +292 -0
  48. package/package.json +25 -151
  49. package/NOTICE +0 -12
  50. package/kestrel.config.ts +0 -28
  51. package/layers/access/nuxt.config.ts +0 -5
  52. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  53. package/layers/access/server/middleware/access-guard.ts +0 -32
  54. package/layers/access/server/types.d.ts +0 -7
  55. package/layers/admin/app/components/AdminAccount.vue +0 -212
  56. package/layers/admin/app/components/AdminNav.vue +0 -44
  57. package/layers/admin/app/components/BlockFields.vue +0 -67
  58. package/layers/admin/app/components/BlockPreview.vue +0 -413
  59. package/layers/admin/app/components/BlockTree.vue +0 -433
  60. package/layers/admin/app/components/BlocksBody.vue +0 -163
  61. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  62. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  63. package/layers/admin/app/components/CollectionList.vue +0 -429
  64. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  65. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  66. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  67. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  68. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  69. package/layers/admin/app/components/EditorStatus.vue +0 -211
  70. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  71. package/layers/admin/app/components/FieldsBody.vue +0 -23
  72. package/layers/admin/app/components/LocaleBar.vue +0 -107
  73. package/layers/admin/app/components/PageFields.vue +0 -129
  74. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  75. package/layers/admin/app/components/SeoFields.vue +0 -210
  76. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  77. package/layers/admin/app/composables/useAuth.ts +0 -68
  78. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  79. package/layers/admin/app/composables/useBlocks.ts +0 -7
  80. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  81. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  82. package/layers/admin/app/composables/useCollections.ts +0 -6
  83. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  84. package/layers/admin/app/composables/useEditForm.ts +0 -402
  85. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  86. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  87. package/layers/admin/app/composables/useListColumns.ts +0 -53
  88. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  89. package/layers/admin/app/composables/useListRows.ts +0 -75
  90. package/layers/admin/app/composables/useListSelection.ts +0 -28
  91. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  92. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  93. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  94. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  95. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  96. package/layers/admin/app/composables/useTheme.ts +0 -36
  97. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  98. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  99. package/layers/admin/app/layouts/admin.vue +0 -266
  100. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  101. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  102. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  103. package/layers/admin/app/pages/admin/index.vue +0 -118
  104. package/layers/admin/app/pages/admin/login.vue +0 -68
  105. package/layers/admin/app/pages/admin/media.vue +0 -28
  106. package/layers/admin/app/pages/admin/references.vue +0 -146
  107. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  108. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  109. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  110. package/layers/admin/app/utils/block-tree.ts +0 -214
  111. package/layers/admin/app/utils/collection-ops.ts +0 -69
  112. package/layers/admin/app/utils/dead-refs.ts +0 -30
  113. package/layers/admin/app/utils/edit-form.ts +0 -267
  114. package/layers/admin/app/utils/editor-expose.ts +0 -36
  115. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  116. package/layers/admin/app/utils/editor-registry.ts +0 -20
  117. package/layers/admin/app/utils/field-keys.ts +0 -11
  118. package/layers/admin/app/utils/list-cell.ts +0 -46
  119. package/layers/admin/app/utils/list-columns.ts +0 -142
  120. package/layers/admin/app/utils/list-query.ts +0 -96
  121. package/layers/admin/app/utils/nav-active.ts +0 -9
  122. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  123. package/layers/admin/app/utils/preview-channel.ts +0 -62
  124. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  125. package/layers/admin/app/utils/reauth.ts +0 -67
  126. package/layers/admin/app/utils/record-title.ts +0 -15
  127. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  128. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  129. package/layers/admin/nuxt.config.ts +0 -20
  130. package/layers/auth/nuxt.config.ts +0 -1
  131. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  132. package/layers/collections/nuxt.config.ts +0 -1
  133. package/layers/collections/server/plugins/01.register.ts +0 -29
  134. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  135. package/layers/core/app/composables/layouts.ts +0 -5
  136. package/layers/core/app/utils/field-factories.ts +0 -129
  137. package/layers/core/app/utils/layouts.ts +0 -38
  138. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  139. package/layers/core/modules/auto-discovery/index.ts +0 -112
  140. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  141. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  142. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  143. package/layers/core/modules/component-namespace/index.ts +0 -44
  144. package/layers/core/modules/component-namespace/shared.ts +0 -24
  145. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  146. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  147. package/layers/core/modules/kestrel/index.ts +0 -144
  148. package/layers/core/modules/plugin-order/index.ts +0 -69
  149. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  150. package/layers/core/nuxt.config.ts +0 -10
  151. package/layers/core/server/api/[...path].ts +0 -98
  152. package/layers/core/server/plugins/00.config.ts +0 -12
  153. package/layers/core/server/plugins/00.migrate.ts +0 -12
  154. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  155. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  156. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  157. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  158. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  159. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  160. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  161. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  162. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  163. package/layers/core/server/tasks/db/migrate.ts +0 -50
  164. package/layers/core/server/utils/server-config.ts +0 -81
  165. package/layers/fields/nuxt.config.ts +0 -1
  166. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  167. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  168. package/layers/media/app/components/KestrelImg.vue +0 -94
  169. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  170. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  171. package/layers/media/app/components/MediaGrid.vue +0 -113
  172. package/layers/media/app/components/MediaImage.vue +0 -70
  173. package/layers/media/app/components/MediaLibrary.vue +0 -456
  174. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  175. package/layers/media/app/components/MediaPathBar.vue +0 -163
  176. package/layers/media/app/components/MediaPicker.vue +0 -19
  177. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  178. package/layers/media/app/components/MediaTable.vue +0 -75
  179. package/layers/media/app/components/MediaThumb.vue +0 -25
  180. package/layers/media/app/components/MediaToolbar.vue +0 -90
  181. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  182. package/layers/media/app/components/MediaViewer.vue +0 -137
  183. package/layers/media/app/components/field/Media.vue +0 -263
  184. package/layers/media/app/composables/useBlurUp.ts +0 -55
  185. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  186. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  187. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  188. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  189. package/layers/media/app/composables/useMediaOps.ts +0 -50
  190. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  191. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  192. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  193. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  194. package/layers/media/app/plugins/field-media.client.ts +0 -6
  195. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  196. package/layers/media/app/utils/dnd.ts +0 -69
  197. package/layers/media/app/utils/library.ts +0 -81
  198. package/layers/media/app/utils/ops.ts +0 -60
  199. package/layers/media/app/utils/picture-model.ts +0 -125
  200. package/layers/media/app/utils/thumbhash.ts +0 -19
  201. package/layers/media/app/utils/variant-name.ts +0 -20
  202. package/layers/media/nuxt.config.ts +0 -7
  203. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  204. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  205. package/layers/media/server/plugins/02.register-media.ts +0 -22
  206. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  207. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  208. package/layers/media/server/tasks/media/backfill.ts +0 -17
  209. package/layers/public/app/app.vue +0 -5
  210. package/layers/public/app/components/BlockRenderer.vue +0 -56
  211. package/layers/public/app/components/KestrelLink.vue +0 -26
  212. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  213. package/layers/public/app/composables/public-page.ts +0 -24
  214. package/layers/public/app/error.vue +0 -12
  215. package/layers/public/app/layouts/default.vue +0 -5
  216. package/layers/public/app/pages/[...slug].vue +0 -239
  217. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  218. package/layers/public/app/utils/block-edit-context.ts +0 -16
  219. package/layers/public/app/utils/json-ld.ts +0 -139
  220. package/layers/public/app/utils/link-href.ts +0 -46
  221. package/layers/public/app/utils/page-head.ts +0 -84
  222. package/layers/public/app/utils/page-layout.ts +0 -18
  223. package/layers/public/app/utils/preview-protocol.ts +0 -117
  224. package/layers/public/app/utils/public-route.ts +0 -46
  225. package/layers/public/app/utils/site-head.ts +0 -43
  226. package/layers/public/modules/deploy-output/index.ts +0 -80
  227. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  228. package/layers/public/modules/prerender-routes/index.ts +0 -88
  229. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  230. package/layers/public/modules/prune-media/index.ts +0 -48
  231. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  232. package/layers/public/nuxt.config.ts +0 -14
  233. package/layers/public/server/delivery-live/serve.ts +0 -111
  234. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  235. package/layers/public/server/pipelines/index.ts +0 -23
  236. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  237. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  238. package/layers/public/server/plugins/02.register-links.ts +0 -16
  239. package/layers/public/server/plugins/03.redirects.ts +0 -55
  240. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  241. package/layers/public/server/plugins/zz.publish.ts +0 -127
  242. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  243. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  244. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  245. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  246. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  247. package/layers/public/server/tasks/publish/run.ts +0 -32
  248. package/layers/public/server/utils/publish/render-live.ts +0 -26
  249. package/layers/ui/app/assets/scss/_base.scss +0 -16
  250. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  251. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  252. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  253. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  254. package/layers/ui/app/assets/scss/main.scss +0 -4
  255. package/layers/ui/app/components/field/Boolean.vue +0 -30
  256. package/layers/ui/app/components/field/Choice.vue +0 -73
  257. package/layers/ui/app/components/field/Datetime.vue +0 -98
  258. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  259. package/layers/ui/app/components/field/Json.vue +0 -62
  260. package/layers/ui/app/components/field/Layout.vue +0 -138
  261. package/layers/ui/app/components/field/Link.vue +0 -112
  262. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  263. package/layers/ui/app/components/field/Number.vue +0 -30
  264. package/layers/ui/app/components/field/Relation.vue +0 -49
  265. package/layers/ui/app/components/field/Renderer.vue +0 -30
  266. package/layers/ui/app/components/field/Repeater.vue +0 -460
  267. package/layers/ui/app/components/field/Richtext.vue +0 -26
  268. package/layers/ui/app/components/field/Slug.vue +0 -65
  269. package/layers/ui/app/components/field/Text.vue +0 -35
  270. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  271. package/layers/ui/app/components/ui/Alert.vue +0 -50
  272. package/layers/ui/app/components/ui/Brand.vue +0 -22
  273. package/layers/ui/app/components/ui/Button.vue +0 -116
  274. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  275. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  276. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  277. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  278. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  279. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  280. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  281. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  282. package/layers/ui/app/components/ui/Field.vue +0 -58
  283. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  284. package/layers/ui/app/components/ui/Icon.vue +0 -44
  285. package/layers/ui/app/components/ui/Menu.vue +0 -54
  286. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  287. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  288. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  289. package/layers/ui/app/components/ui/Select.vue +0 -52
  290. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  291. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  292. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  293. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  294. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  295. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  296. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  297. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  298. package/layers/ui/app/composables/useLinkField.ts +0 -70
  299. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  300. package/layers/ui/app/composables/useRepeater.ts +0 -99
  301. package/layers/ui/app/composables/useT.ts +0 -32
  302. package/layers/ui/app/composables/useToast.ts +0 -46
  303. package/layers/ui/app/i18n/de.ts +0 -432
  304. package/layers/ui/app/i18n/en.ts +0 -434
  305. package/layers/ui/app/utils/boolean-field.ts +0 -10
  306. package/layers/ui/app/utils/field-component.ts +0 -22
  307. package/layers/ui/app/utils/field-empty.ts +0 -21
  308. package/layers/ui/app/utils/field-registry.ts +0 -36
  309. package/layers/ui/app/utils/humanize.ts +0 -14
  310. package/layers/ui/app/utils/icons.ts +0 -143
  311. package/layers/ui/app/utils/iso-date.ts +0 -18
  312. package/layers/ui/app/utils/localized.ts +0 -13
  313. package/layers/ui/app/utils/record-context.ts +0 -15
  314. package/layers/ui/app/utils/reorder.ts +0 -8
  315. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  316. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  317. package/layers/ui/nuxt.config.ts +0 -9
  318. package/nuxt.config.ts +0 -108
  319. package/scripts/consumer-template-ci.mjs +0 -160
  320. package/scripts/copy-create-payload.mjs +0 -49
  321. package/scripts/coverage-all.mjs +0 -163
  322. package/scripts/dashboard.mjs +0 -89
  323. package/scripts/hash-password.mjs +0 -29
  324. package/scripts/kestrel.mjs +0 -240
  325. package/scripts/lib/cli.mjs +0 -129
  326. package/scripts/lib/coverage-model.mjs +0 -51
  327. package/scripts/lib/password.mjs +0 -19
  328. package/scripts/lib/scaffold.mjs +0 -205
  329. package/scripts/port-graph.mjs +0 -350
  330. package/scripts/typecheck.mjs +0 -62
  331. package/templates/starter/README.md +0 -65
  332. package/templates/starter/_env.example +0 -17
  333. package/templates/starter/_gitignore +0 -26
  334. package/templates/starter/_package.json +0 -22
  335. package/templates/starter/app/app.vue +0 -7
  336. package/templates/starter/app/blocks/Prose.vue +0 -13
  337. package/templates/starter/app/layouts/default.vue +0 -6
  338. package/templates/starter/nuxt.config.ts +0 -23
  339. package/templates/starter/pnpm-workspace.yaml +0 -8
  340. 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
- }