@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.
Files changed (342) hide show
  1. package/README.md +26 -201
  2. package/dist/boot.d.ts +45 -0
  3. package/dist/boot.js +284 -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 +162 -0
  15. package/dist/defineConfig.js +42 -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/allowlist.d.ts +9 -0
  43. package/dist/triggers/allowlist.js +49 -0
  44. package/dist/triggers/cron.d.ts +10 -0
  45. package/dist/triggers/cron.js +44 -0
  46. package/dist/triggers/cronMatch.d.ts +10 -0
  47. package/dist/triggers/cronMatch.js +68 -0
  48. package/dist/triggers/http.d.ts +59 -0
  49. package/dist/triggers/http.js +300 -0
  50. package/package.json +25 -151
  51. package/NOTICE +0 -12
  52. package/kestrel.config.ts +0 -28
  53. package/layers/access/nuxt.config.ts +0 -5
  54. package/layers/access/server/middleware/00.ip-allowlist.ts +0 -52
  55. package/layers/access/server/middleware/access-guard.ts +0 -32
  56. package/layers/access/server/types.d.ts +0 -7
  57. package/layers/admin/app/components/AdminAccount.vue +0 -212
  58. package/layers/admin/app/components/AdminNav.vue +0 -44
  59. package/layers/admin/app/components/BlockFields.vue +0 -67
  60. package/layers/admin/app/components/BlockPreview.vue +0 -413
  61. package/layers/admin/app/components/BlockTree.vue +0 -433
  62. package/layers/admin/app/components/BlocksBody.vue +0 -163
  63. package/layers/admin/app/components/CollectionDeleteDialog.vue +0 -38
  64. package/layers/admin/app/components/CollectionEditor.vue +0 -324
  65. package/layers/admin/app/components/CollectionList.vue +0 -429
  66. package/layers/admin/app/components/CollectionListBulkBar.vue +0 -62
  67. package/layers/admin/app/components/CollectionListColumnsPanel.vue +0 -37
  68. package/layers/admin/app/components/CollectionListFilterPanel.vue +0 -145
  69. package/layers/admin/app/components/CollectionListPager.vue +0 -70
  70. package/layers/admin/app/components/CollectionListTable.vue +0 -347
  71. package/layers/admin/app/components/EditorStatus.vue +0 -211
  72. package/layers/admin/app/components/EditorUnsupported.vue +0 -28
  73. package/layers/admin/app/components/FieldsBody.vue +0 -23
  74. package/layers/admin/app/components/LocaleBar.vue +0 -107
  75. package/layers/admin/app/components/PageFields.vue +0 -129
  76. package/layers/admin/app/components/PageFieldsPane.vue +0 -39
  77. package/layers/admin/app/components/SeoFields.vue +0 -210
  78. package/layers/admin/app/components/SingletonEditor.vue +0 -93
  79. package/layers/admin/app/composables/useAuth.ts +0 -68
  80. package/layers/admin/app/composables/useBlockTree.ts +0 -95
  81. package/layers/admin/app/composables/useBlocks.ts +0 -7
  82. package/layers/admin/app/composables/useCachedResource.ts +0 -35
  83. package/layers/admin/app/composables/useCollectionOps.ts +0 -102
  84. package/layers/admin/app/composables/useCollections.ts +0 -6
  85. package/layers/admin/app/composables/useContentLocales.ts +0 -23
  86. package/layers/admin/app/composables/useEditForm.ts +0 -402
  87. package/layers/admin/app/composables/useLinkResolver.ts +0 -40
  88. package/layers/admin/app/composables/useListBatchActions.ts +0 -78
  89. package/layers/admin/app/composables/useListColumns.ts +0 -53
  90. package/layers/admin/app/composables/useListFilterDraft.ts +0 -129
  91. package/layers/admin/app/composables/useListRows.ts +0 -75
  92. package/layers/admin/app/composables/useListSelection.ts +0 -28
  93. package/layers/admin/app/composables/useListUrlState.ts +0 -53
  94. package/layers/admin/app/composables/usePendingPublishPoll.ts +0 -15
  95. package/layers/admin/app/composables/usePreviewViewport.ts +0 -30
  96. package/layers/admin/app/composables/usePublishStatus.ts +0 -107
  97. package/layers/admin/app/composables/useRailCollapsed.ts +0 -14
  98. package/layers/admin/app/composables/useTheme.ts +0 -36
  99. package/layers/admin/app/composables/useToolbarPanel.ts +0 -38
  100. package/layers/admin/app/composables/useUnsavedGuard.ts +0 -22
  101. package/layers/admin/app/layouts/admin.vue +0 -266
  102. package/layers/admin/app/middleware/admin-auth.ts +0 -4
  103. package/layers/admin/app/pages/admin/[collection]/[id].vue +0 -203
  104. package/layers/admin/app/pages/admin/[collection]/index.vue +0 -62
  105. package/layers/admin/app/pages/admin/index.vue +0 -118
  106. package/layers/admin/app/pages/admin/login.vue +0 -68
  107. package/layers/admin/app/pages/admin/media.vue +0 -28
  108. package/layers/admin/app/pages/admin/references.vue +0 -146
  109. package/layers/admin/app/plugins/admin-style-guard.client.ts +0 -76
  110. package/layers/admin/app/plugins/reauth.client.ts +0 -20
  111. package/layers/admin/app/utils/admin-style-guard.ts +0 -20
  112. package/layers/admin/app/utils/block-tree.ts +0 -214
  113. package/layers/admin/app/utils/collection-ops.ts +0 -69
  114. package/layers/admin/app/utils/dead-refs.ts +0 -30
  115. package/layers/admin/app/utils/edit-form.ts +0 -267
  116. package/layers/admin/app/utils/editor-expose.ts +0 -36
  117. package/layers/admin/app/utils/editor-form-context.ts +0 -82
  118. package/layers/admin/app/utils/editor-registry.ts +0 -20
  119. package/layers/admin/app/utils/field-keys.ts +0 -11
  120. package/layers/admin/app/utils/list-cell.ts +0 -46
  121. package/layers/admin/app/utils/list-columns.ts +0 -142
  122. package/layers/admin/app/utils/list-query.ts +0 -96
  123. package/layers/admin/app/utils/nav-active.ts +0 -9
  124. package/layers/admin/app/utils/populate-blocks.ts +0 -288
  125. package/layers/admin/app/utils/preview-channel.ts +0 -62
  126. package/layers/admin/app/utils/preview-viewport.ts +0 -80
  127. package/layers/admin/app/utils/reauth.ts +0 -67
  128. package/layers/admin/app/utils/record-title.ts +0 -15
  129. package/layers/admin/app/utils/register-builtin-editors.ts +0 -9
  130. package/layers/admin/app/utils/safe-redirect.ts +0 -12
  131. package/layers/admin/nuxt.config.ts +0 -20
  132. package/layers/auth/nuxt.config.ts +0 -1
  133. package/layers/auth/server/plugins/01.register-auth-pipelines.ts +0 -7
  134. package/layers/collections/nuxt.config.ts +0 -1
  135. package/layers/collections/server/plugins/01.register.ts +0 -29
  136. package/layers/collections/server/plugins/02.register-relation-populate.ts +0 -18
  137. package/layers/core/app/composables/layouts.ts +0 -5
  138. package/layers/core/app/utils/field-factories.ts +0 -129
  139. package/layers/core/app/utils/layouts.ts +0 -38
  140. package/layers/core/modules/auto-discovery/extract-block.ts +0 -179
  141. package/layers/core/modules/auto-discovery/index.ts +0 -112
  142. package/layers/core/modules/auto-discovery/package-registry.ts +0 -19
  143. package/layers/core/modules/auto-discovery/scan.ts +0 -170
  144. package/layers/core/modules/auto-discovery/virtual.d.ts +0 -20
  145. package/layers/core/modules/component-namespace/index.ts +0 -44
  146. package/layers/core/modules/component-namespace/shared.ts +0 -24
  147. package/layers/core/modules/kestrel/app-shell.ts +0 -55
  148. package/layers/core/modules/kestrel/auto-imports.ts +0 -44
  149. package/layers/core/modules/kestrel/index.ts +0 -144
  150. package/layers/core/modules/plugin-order/index.ts +0 -69
  151. package/layers/core/modules/plugin-order/plugin-order.ts +0 -273
  152. package/layers/core/nuxt.config.ts +0 -10
  153. package/layers/core/server/api/[...path].ts +0 -98
  154. package/layers/core/server/plugins/00.config.ts +0 -12
  155. package/layers/core/server/plugins/00.migrate.ts +0 -12
  156. package/layers/core/server/plugins/01.register-introspection-pipeline.ts +0 -5
  157. package/layers/core/server/plugins/01.register-openapi-pipeline.ts +0 -5
  158. package/layers/core/server/plugins/01.register-tooling-pipelines.ts +0 -5
  159. package/layers/core/server/plugins/02.schema-sync.ts +0 -58
  160. package/layers/core/server/plugins/04.outbox-worker.ts +0 -31
  161. package/layers/core/server/plugins/05.reindex-refs.ts +0 -5
  162. package/layers/core/server/routes/__kestrel/dashboard.get.ts +0 -29
  163. package/layers/core/server/tasks/db/migrate-module.ts +0 -83
  164. package/layers/core/server/tasks/db/migrate-revisions.ts +0 -20
  165. package/layers/core/server/tasks/db/migrate.ts +0 -50
  166. package/layers/core/server/utils/server-config.ts +0 -81
  167. package/layers/fields/nuxt.config.ts +0 -1
  168. package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +0 -7
  169. package/layers/fields/server/plugins/01.register-field-populate.ts +0 -11
  170. package/layers/media/app/components/KestrelImg.vue +0 -94
  171. package/layers/media/app/components/MediaConflictDialog.vue +0 -35
  172. package/layers/media/app/components/MediaDeleteDialog.vue +0 -52
  173. package/layers/media/app/components/MediaGrid.vue +0 -113
  174. package/layers/media/app/components/MediaImage.vue +0 -70
  175. package/layers/media/app/components/MediaLibrary.vue +0 -456
  176. package/layers/media/app/components/MediaNewFolderDialog.vue +0 -32
  177. package/layers/media/app/components/MediaPathBar.vue +0 -163
  178. package/layers/media/app/components/MediaPicker.vue +0 -19
  179. package/layers/media/app/components/MediaRenameDialog.vue +0 -32
  180. package/layers/media/app/components/MediaTable.vue +0 -75
  181. package/layers/media/app/components/MediaThumb.vue +0 -25
  182. package/layers/media/app/components/MediaToolbar.vue +0 -90
  183. package/layers/media/app/components/MediaUploadDialog.vue +0 -87
  184. package/layers/media/app/components/MediaViewer.vue +0 -137
  185. package/layers/media/app/components/field/Media.vue +0 -263
  186. package/layers/media/app/composables/useBlurUp.ts +0 -55
  187. package/layers/media/app/composables/useMediaClipboard.ts +0 -14
  188. package/layers/media/app/composables/useMediaContextMenu.ts +0 -56
  189. package/layers/media/app/composables/useMediaDnd.ts +0 -56
  190. package/layers/media/app/composables/useMediaLibrary.ts +0 -152
  191. package/layers/media/app/composables/useMediaOps.ts +0 -50
  192. package/layers/media/app/composables/useMediaRelocate.ts +0 -39
  193. package/layers/media/app/composables/useMediaResolver.ts +0 -50
  194. package/layers/media/app/composables/useMediaUpload.ts +0 -93
  195. package/layers/media/app/composables/useMediaVariant.ts +0 -34
  196. package/layers/media/app/plugins/field-media.client.ts +0 -6
  197. package/layers/media/app/utils/ai-disclosure.ts +0 -19
  198. package/layers/media/app/utils/dnd.ts +0 -69
  199. package/layers/media/app/utils/library.ts +0 -81
  200. package/layers/media/app/utils/ops.ts +0 -60
  201. package/layers/media/app/utils/picture-model.ts +0 -125
  202. package/layers/media/app/utils/thumbhash.ts +0 -19
  203. package/layers/media/app/utils/variant-name.ts +0 -20
  204. package/layers/media/nuxt.config.ts +0 -7
  205. package/layers/media/server/middleware/ondemand-variants.ts +0 -38
  206. package/layers/media/server/plugins/01.register-media-pipelines.ts +0 -7
  207. package/layers/media/server/plugins/02.register-media.ts +0 -22
  208. package/layers/media/server/plugins/04.variant-capture.ts +0 -27
  209. package/layers/media/server/plugins/05.media-cleanup.ts +0 -5
  210. package/layers/media/server/tasks/media/backfill.ts +0 -17
  211. package/layers/public/app/app.vue +0 -5
  212. package/layers/public/app/components/BlockRenderer.vue +0 -56
  213. package/layers/public/app/components/KestrelLink.vue +0 -26
  214. package/layers/public/app/components/KestrelPreviewBridge.vue +0 -133
  215. package/layers/public/app/composables/public-page.ts +0 -24
  216. package/layers/public/app/error.vue +0 -12
  217. package/layers/public/app/layouts/default.vue +0 -5
  218. package/layers/public/app/pages/[...slug].vue +0 -239
  219. package/layers/public/app/pages/__kestrel/preview.vue +0 -46
  220. package/layers/public/app/utils/block-edit-context.ts +0 -16
  221. package/layers/public/app/utils/json-ld.ts +0 -139
  222. package/layers/public/app/utils/link-href.ts +0 -46
  223. package/layers/public/app/utils/page-head.ts +0 -84
  224. package/layers/public/app/utils/page-layout.ts +0 -18
  225. package/layers/public/app/utils/preview-protocol.ts +0 -117
  226. package/layers/public/app/utils/public-route.ts +0 -46
  227. package/layers/public/app/utils/site-head.ts +0 -43
  228. package/layers/public/modules/deploy-output/index.ts +0 -80
  229. package/layers/public/modules/prerender-routes/discover.ts +0 -37
  230. package/layers/public/modules/prerender-routes/index.ts +0 -88
  231. package/layers/public/modules/prerender-routes/routes.ts +0 -16
  232. package/layers/public/modules/prune-media/index.ts +0 -48
  233. package/layers/public/modules/prune-media/prune-media.ts +0 -81
  234. package/layers/public/nuxt.config.ts +0 -14
  235. package/layers/public/server/delivery-live/serve.ts +0 -111
  236. package/layers/public/server/middleware/delivery-live-catchall.ts +0 -7
  237. package/layers/public/server/pipelines/index.ts +0 -23
  238. package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +0 -14
  239. package/layers/public/server/plugins/01.register-public-pipelines.ts +0 -7
  240. package/layers/public/server/plugins/02.register-links.ts +0 -16
  241. package/layers/public/server/plugins/03.redirects.ts +0 -55
  242. package/layers/public/server/plugins/05.plan-publish.ts +0 -5
  243. package/layers/public/server/plugins/zz.publish.ts +0 -127
  244. package/layers/public/server/routes/llms-full.txt.get.ts +0 -99
  245. package/layers/public/server/routes/llms.txt.get.ts +0 -66
  246. package/layers/public/server/routes/redirects.json.get.ts +0 -58
  247. package/layers/public/server/routes/robots.txt.get.ts +0 -11
  248. package/layers/public/server/routes/sitemap.xml.get.ts +0 -79
  249. package/layers/public/server/tasks/publish/run.ts +0 -32
  250. package/layers/public/server/utils/publish/render-live.ts +0 -26
  251. package/layers/ui/app/assets/scss/_base.scss +0 -16
  252. package/layers/ui/app/assets/scss/_datepicker.scss +0 -82
  253. package/layers/ui/app/assets/scss/_mixins.scss +0 -50
  254. package/layers/ui/app/assets/scss/_reset.scss +0 -65
  255. package/layers/ui/app/assets/scss/_tokens.scss +0 -183
  256. package/layers/ui/app/assets/scss/main.scss +0 -4
  257. package/layers/ui/app/components/field/Boolean.vue +0 -30
  258. package/layers/ui/app/components/field/Choice.vue +0 -73
  259. package/layers/ui/app/components/field/Datetime.vue +0 -98
  260. package/layers/ui/app/components/field/DeadRefNote.vue +0 -28
  261. package/layers/ui/app/components/field/Json.vue +0 -62
  262. package/layers/ui/app/components/field/Layout.vue +0 -138
  263. package/layers/ui/app/components/field/Link.vue +0 -112
  264. package/layers/ui/app/components/field/LinkInternalPicker.vue +0 -95
  265. package/layers/ui/app/components/field/Number.vue +0 -30
  266. package/layers/ui/app/components/field/Relation.vue +0 -49
  267. package/layers/ui/app/components/field/Renderer.vue +0 -30
  268. package/layers/ui/app/components/field/Repeater.vue +0 -460
  269. package/layers/ui/app/components/field/Richtext.vue +0 -26
  270. package/layers/ui/app/components/field/Slug.vue +0 -65
  271. package/layers/ui/app/components/field/Text.vue +0 -35
  272. package/layers/ui/app/components/field/Unsupported.vue +0 -34
  273. package/layers/ui/app/components/ui/Alert.vue +0 -50
  274. package/layers/ui/app/components/ui/Brand.vue +0 -22
  275. package/layers/ui/app/components/ui/Button.vue +0 -116
  276. package/layers/ui/app/components/ui/ButtonGroup.vue +0 -96
  277. package/layers/ui/app/components/ui/Checkbox.vue +0 -53
  278. package/layers/ui/app/components/ui/CheckboxGroup.vue +0 -51
  279. package/layers/ui/app/components/ui/Combobox.vue +0 -345
  280. package/layers/ui/app/components/ui/DatePicker.vue +0 -92
  281. package/layers/ui/app/components/ui/DateRangePicker.vue +0 -100
  282. package/layers/ui/app/components/ui/Dialog.vue +0 -107
  283. package/layers/ui/app/components/ui/EmptyState.vue +0 -46
  284. package/layers/ui/app/components/ui/Field.vue +0 -58
  285. package/layers/ui/app/components/ui/Fieldset.vue +0 -65
  286. package/layers/ui/app/components/ui/Icon.vue +0 -44
  287. package/layers/ui/app/components/ui/Menu.vue +0 -54
  288. package/layers/ui/app/components/ui/NumberInput.vue +0 -175
  289. package/layers/ui/app/components/ui/Richtext.vue +0 -141
  290. package/layers/ui/app/components/ui/RichtextToolbar.vue +0 -251
  291. package/layers/ui/app/components/ui/Select.vue +0 -52
  292. package/layers/ui/app/components/ui/TextInput.vue +0 -111
  293. package/layers/ui/app/components/ui/Textarea.vue +0 -39
  294. package/layers/ui/app/components/ui/TimeInput.vue +0 -18
  295. package/layers/ui/app/components/ui/Toasts.vue +0 -103
  296. package/layers/ui/app/components/ui/Tooltip.vue +0 -45
  297. package/layers/ui/app/composables/useAdminLang.ts +0 -8
  298. package/layers/ui/app/composables/useDragReorder.ts +0 -41
  299. package/layers/ui/app/composables/useEchoGuard.ts +0 -28
  300. package/layers/ui/app/composables/useLinkField.ts +0 -70
  301. package/layers/ui/app/composables/useRecordOptions.ts +0 -84
  302. package/layers/ui/app/composables/useRepeater.ts +0 -99
  303. package/layers/ui/app/composables/useT.ts +0 -32
  304. package/layers/ui/app/composables/useToast.ts +0 -46
  305. package/layers/ui/app/i18n/de.ts +0 -432
  306. package/layers/ui/app/i18n/en.ts +0 -434
  307. package/layers/ui/app/utils/boolean-field.ts +0 -10
  308. package/layers/ui/app/utils/field-component.ts +0 -22
  309. package/layers/ui/app/utils/field-empty.ts +0 -21
  310. package/layers/ui/app/utils/field-registry.ts +0 -36
  311. package/layers/ui/app/utils/humanize.ts +0 -14
  312. package/layers/ui/app/utils/icons.ts +0 -143
  313. package/layers/ui/app/utils/iso-date.ts +0 -18
  314. package/layers/ui/app/utils/localized.ts +0 -13
  315. package/layers/ui/app/utils/record-context.ts +0 -15
  316. package/layers/ui/app/utils/reorder.ts +0 -8
  317. package/layers/ui/app/utils/richtext-preserve-class.ts +0 -31
  318. package/layers/ui/app/utils/useFieldA11y.ts +0 -40
  319. package/layers/ui/nuxt.config.ts +0 -9
  320. package/nuxt.config.ts +0 -108
  321. package/scripts/consumer-template-ci.mjs +0 -160
  322. package/scripts/copy-create-payload.mjs +0 -49
  323. package/scripts/coverage-all.mjs +0 -163
  324. package/scripts/dashboard.mjs +0 -89
  325. package/scripts/hash-password.mjs +0 -29
  326. package/scripts/kestrel.mjs +0 -240
  327. package/scripts/lib/cli.mjs +0 -129
  328. package/scripts/lib/coverage-model.mjs +0 -51
  329. package/scripts/lib/password.mjs +0 -19
  330. package/scripts/lib/scaffold.mjs +0 -205
  331. package/scripts/port-graph.mjs +0 -350
  332. package/scripts/typecheck.mjs +0 -62
  333. package/templates/starter/README.md +0 -65
  334. package/templates/starter/_env.example +0 -17
  335. package/templates/starter/_gitignore +0 -26
  336. package/templates/starter/_package.json +0 -22
  337. package/templates/starter/app/app.vue +0 -7
  338. package/templates/starter/app/blocks/Prose.vue +0 -13
  339. package/templates/starter/app/layouts/default.vue +0 -6
  340. package/templates/starter/nuxt.config.ts +0 -23
  341. package/templates/starter/pnpm-workspace.yaml +0 -8
  342. package/templates/starter/tsconfig.json +0 -3
@@ -1,324 +0,0 @@
1
- <script setup lang="ts">
2
- import { localePath } from '@michaelthielemann/kestrel-core/client'
3
- import { PREVIEW_TOKEN_QUERY, PREVIEW_FALLBACK_PATH } from '../../../public/app/utils/preview-protocol'
4
- import { resolveCollectionEditor } from '../utils/editor-registry'
5
- import { editorFormContextKey } from '../utils/editor-form-context'
6
- import '../utils/register-builtin-editors'
7
-
8
- const props = withDefaults(
9
- // `actions`: render the inline Save/Cancel row. The record editor hides it and drives the save from
10
- // its page header (where Delete also lives) via a `type=submit` button tied to this form by `formId`;
11
- // the singleton collection page keeps the inline actions. `formId` ids the form for that association.
12
- defineProps<{ collection: string; id: string; localeParam?: string; group?: string; actions?: boolean; formId?: string }>(),
13
- { actions: true },
14
- )
15
- const emit = defineEmits<{ saved: [record: unknown]; cancel: [] }>()
16
-
17
- const saved = ref(false)
18
- const { t } = useT()
19
- const toast = useToast()
20
- const f = useEditForm({ collection: props.collection, id: props.id, locale: props.localeParam, group: props.group })
21
- // `useEditForm` returns its refs/computeds synchronously (only its internal `init()` is async), so this
22
- // destructure is safe to read before `f.ready` resolves — pulled up here (rather than after the await
23
- // below, where it originally lived) so `defineExpose` can also run before that same await.
24
- const {
25
- formError, saving, submit, dirty, editorType, pageLike, hasStatus,
26
- savedStatus, undo, redo, canUndo, canRedo, quarantined,
27
- } = f
28
-
29
- // Editor → field-widget context. MUST run BEFORE the top-level `await` below — after an await the setup
30
- // instance context is gone and `provide` silently no-ops. `id`/`saved` let a widget clean up an abandoned
31
- // never-saved draft (the secure-gallery namespace); `values` (reactive, fills after `ready`) lets it read
32
- // sibling fields (the gallery slug = the proofing key). Mirrors the blockEdit provide.
33
- provide(recordEditContextKey, { id: toRef(props, 'id'), saved, values: f.values })
34
-
35
- // Editor → body context. The shell owns the form/save/history/expose; the resolved body only lays out the
36
- // values. Provided (like recordEditContextKey) BEFORE the await — the computeds below are lazy, so building
37
- // them against `f`'s refs before `ready` resolves is safe. `revealError` is registered by a body (the
38
- // blocks body focuses the offending block) and called by `onSave` on failure, keeping the shell generic.
39
- const renderable = computed(() =>
40
- Object.fromEntries(Object.entries(f.renderableFields.value).map(([name, field]) => [name, asFieldDef(field)])),
41
- )
42
- const rootDeadFields = computed(() => deadFieldsAt(f.deadRefs.value, null))
43
- // The record's own title for the header heading (blank → the host falls back to "Edit {collection} #{id}").
44
- const heading = computed(() => recordTitle(renderable.value, f.values))
45
- // Page/root field pane bindings — rendered by both the fields body and the blocks 3-pane root. Built once
46
- // here so the two bodies differ only in layout chrome.
47
- const pageFieldsBindings = computed(() => ({
48
- translatable: f.translatable.value, collection: props.collection, id: props.id, mode: f.mode.value,
49
- locale: f.locale.value, translations: f.translations.value, group: f.translationGroup.value,
50
- fields: renderable.value, fieldLayout: f.fieldLayout.value, values: f.values, errors: f.errors, deadFields: rootDeadFields.value,
51
- pageLike: f.pageLike.value, seo: f.hasSeo.value, status: f.hasStatus.value, disabled: f.saving.value,
52
- }))
53
- // camelCase keys: `v-on="obj"` does not camelize keys at runtime, so a kebab `copy-from` key becomes the
54
- // `onCopy-from` listener — which never matches PageFieldsPane's declared `copyFrom` emit. The declared
55
- // emit names are the contract.
56
- const pageFieldsHandlers = { copyFrom: onCopyFrom, update: f.setField }
57
-
58
- // Live-preview affordances (the header's "open in new tab" link, the status Ampel, the preview
59
- // iframe): the current publish status and the record's real public URL (localized), or null when the
60
- // record has no routable path (non-pageLike, never saved). Built from the SAVED path (baseline) — an
61
- // unsaved slug edit must not point the preview/link at a URL that does not exist yet; a save moves the
62
- // baseline and both follow. Defined BEFORE the provide so the blocks body can read `previewUrl` from
63
- // the context (composable + lazy computeds — safe pre-`ready`).
64
- const { primary, prefixPrimary } = useContentLocales()
65
- const status = computed(() => (f.values.status as string | undefined) ?? '')
66
- const previewUrl = computed(() => {
67
- const path = f.savedPath.value
68
- if (!f.pageLike.value || !path || props.id === 'new') return null
69
- return localePath(path, f.locale.value, primary, prefixPrimary)
70
- })
71
-
72
- let revealError: (() => void) | null = null
73
- provide(editorFormContextKey, {
74
- values: f.values, errors: f.errors, formError: f.formError, blockErrors: f.blockErrors, setField: f.setField,
75
- locale: f.locale, translations: f.translations, translationGroup: f.translationGroup, deadRefs: f.deadRefs,
76
- saving: f.saving, mode: f.mode, translatable: f.translatable, pageLike: f.pageLike, hasSeo: f.hasSeo,
77
- hasStatus: f.hasStatus, blocksAllowed: f.blocksAllowed, renderable, previewUrl, undo: f.undo, redo: f.redo,
78
- applyFrom: f.applyFrom, pageFieldsBindings, pageFieldsHandlers,
79
- registerRevealError: (fn: () => void) => { revealError = fn },
80
- })
81
-
82
- // Right dot of the editor Ampel: the live / generated state of THIS record's static page. Only a saved
83
- // pageLike record has one (a non-pageLike collection produces no static page; an unsaved `new` has no row).
84
- // Refreshed on mount and after each save (a save may (re)publish the page).
85
- const liveStatus = usePublishStatus({
86
- collection: () => props.collection,
87
- id: () => props.id,
88
- locale: () => f.locale.value,
89
- enabled: () => pageLike.value && props.id !== 'new',
90
- })
91
- // Carries "poll the live status on mount" across the create→navigate remount (see usePendingPublishPoll).
92
- const pendingPoll = usePendingPublishPoll()
93
-
94
- // Saving persists to the DB and leaves the live site alone; publishing writes the static file(s). The two
95
- // are separate buttons because they are separate decisions (ADR-0008) — you can save a page a dozen times
96
- // while the published version stays exactly as it was.
97
- const publishing = ref(false)
98
- const previewOpening = ref(false)
99
- // `output.publishOnSave` turns the split off again (a save republishes) — then a Publish
100
- // button would have nothing to do, so the hosts hide it. Reported by /api/publishStatus; unknown (a
101
- // never-saved record, which does not fetch it) reads as the default, i.e. the button stays.
102
- const publishOnSave = computed(() => liveStatus.data.value.publishOnSave === true)
103
- const canPublish = computed(() => !publishOnSave.value)
104
-
105
- /** Save (a publish publishes what you SEE), promote a draft, then write the output. */
106
- async function publish() {
107
- if (publishing.value || saving.value || quarantined.value) return
108
- publishing.value = true
109
- try {
110
- // Pressing Publish IS the publish intent, so a draft is promoted here rather than sending the user to
111
- // the status select first. A statusless collection has nothing to promote.
112
- if (hasStatus.value && f.values.status !== 'published') f.setField('status', 'published')
113
- const r = await submit()
114
- if (!r.ok) {
115
- revealError?.()
116
- toast.error(formError.value || t('editor.saveFailed'))
117
- return
118
- }
119
- saved.value = true
120
- // The saved row's id, whatever the route param says — `single`/`new` are not ids.
121
- const id = (r.record as { id?: number } | null)?.id ?? Number(props.id)
122
- if (!Number.isInteger(id)) return
123
- const since = liveStatus.data.value.updatedAt ?? null
124
- // Awaited BEFORE `saved` is emitted: on a new record that emit navigates to the record's own URL and
125
- // tears this instance down, which would cut the request short.
126
- const res = await $fetch<{ generates: boolean; drafts: number[] }>('/api/publish', {
127
- method: 'POST', body: { collection: props.collection, id },
128
- }).catch(() => null)
129
- // A create navigates to the new record's URL (a full remount), so this instance is gone before its
130
- // poll could run — hand the intent to the arriving one instead.
131
- if (props.id === 'new') pendingPoll.value = pageLike.value && !!res?.generates
132
- emit('saved', r.record)
133
- if (!res) { toast.error(t('editor.publishFailed')); return }
134
- if (res.drafts.length) { toast.info(t('editor.publishDraft')); return }
135
- if (!res.generates) { toast.info(t('editor.publishNotGenerated')); return }
136
- toast.success(t('toast.published'))
137
- void liveStatus.refreshUntilSettled({ since })
138
- } finally {
139
- publishing.value = false
140
- }
141
- }
142
-
143
- /**
144
- * Open the record in a new tab. Unsaved edits travel as a preview TICKET (a token in the URL) instead of
145
- * being written to the DB: previewing must never be an unasked-for save. A saved, unmodified record just
146
- * opens its URL.
147
- *
148
- * The tab is opened SYNCHRONOUSLY (about:blank) and redirected once the ticket is minted — a popup opened
149
- * after an await is a popup blocker's textbook case. That costs `noopener`, which the direct path keeps;
150
- * the target is our own origin either way.
151
- */
152
- async function openPreview() {
153
- const url = previewUrl.value
154
- if (!dirty.value && props.id !== 'new' && url) {
155
- window.open(url, '_blank', 'noopener,noreferrer')
156
- return
157
- }
158
- const tab = window.open('', '_blank')
159
- previewOpening.value = true
160
- try {
161
- const ticket = await $fetch<{ token: string }>('/api/createPreview', {
162
- method: 'POST',
163
- body: {
164
- collection: props.collection,
165
- id: Number.isInteger(Number(props.id)) ? Number(props.id) : null, // 'new' / 'single' have no id
166
- locale: f.locale.value,
167
- values: f.buildBody(),
168
- },
169
- }).catch(() => null)
170
- if (!ticket) {
171
- tab?.close()
172
- toast.error(t('editor.previewFailed'))
173
- return
174
- }
175
- // No public URL (never saved, blank slug, non-pageLike) → the dedicated preview page renders the
176
- // ticket in the real public app instead.
177
- const base = url ?? `${PREVIEW_FALLBACK_PATH}?locale=${encodeURIComponent(f.locale.value)}`
178
- const target = `${base}${base.includes('?') ? '&' : '?'}${PREVIEW_TOKEN_QUERY}=${encodeURIComponent(ticket.token)}`
179
- if (tab) tab.location.replace(target)
180
- else window.open(target, '_blank', 'noopener,noreferrer') // popup blocked → try once more, directly
181
- } finally {
182
- previewOpening.value = false
183
- }
184
- }
185
-
186
- // The record-editor page renders the action toolbar (Save/Cancel/Delete + Undo/Redo) in its header: it
187
- // submits this form by id (formId), reads the in-flight `saving` for the button state, drives the
188
- // unsaved-changes guard off `dirty`, and the undo/redo controls off the history API exposed here.
189
- // The exposed shape IS the `EditorExpose` contract (utils/editor-expose.ts) — keep them in sync.
190
- // MUST run BEFORE the `await f.ready` below — Vue does not guarantee an async setup component's expose is
191
- // wired if `defineExpose` runs after a top-level await (the parent's template ref may resolve first).
192
- defineExpose({
193
- dirty, saving, undo, redo, canUndo, canRedo, hasStatus, status, savedStatus, previewUrl, pageLike,
194
- live: liveStatus.data, recordTitle: heading, publish, publishing, canPublish, openPreview, previewOpening,
195
- quarantined,
196
- })
197
-
198
- await f.ready
199
-
200
- // Which editor body renders (fields · blocks · an extension type). Resolved from the registry; an
201
- // unregistered type falls back to the visible EditorUnsupported panel.
202
- const bodyComponent = computed(() => resolveCollectionEditor(editorType.value))
203
-
204
- async function onSave() {
205
- if (quarantined.value) return
206
- // Read BEFORE the save: a successful submit rebaselines, after which the saved status is the new one.
207
- const wasPublished = savedStatus.value === 'published'
208
- const r = await submit()
209
- if (r.ok) {
210
- saved.value = true
211
- // A save publishes nothing (ADR-0008), so there is no in-flight republish to poll for — one refresh,
212
- // which is what turns the right lamp to "Outdated": still live, now an older version than the record.
213
- // An unpublish is the exception the write path still acts on, and its prune is enqueued the same
214
- // debounced way, so that case polls until the row clears.
215
- // …with one exception in each direction: an unpublish IS acted on at save time (its prune rides the same
216
- // debounced queue), and with `publishOnSave` every save republishes, so both poll until the row settles.
217
- const unpublished = hasStatus.value && wasPublished && status.value === 'draft'
218
- const settles = unpublished || (publishOnSave.value && status.value !== 'draft')
219
- const since = liveStatus.data.value.updatedAt ?? null
220
- emit('saved', r.record)
221
- if (props.id !== 'new') void (settles ? liveStatus.refreshUntilSettled({ since }) : liveStatus.refresh())
222
- return
223
- }
224
- // Save failed — let the active body reveal the problem (the blocks body focuses the offending block /
225
- // deselects to show the page fields); then guarantee the failure is noticed on a long editor.
226
- revealError?.()
227
- toast.error(formError.value || t('editor.saveFailed'))
228
- }
229
-
230
- // LocaleBar "copy" — pull a sibling locale's content into the current form (multi only; the button is
231
- // shown only for an existing sibling, so its id is in the translations map).
232
- async function onCopyFrom(loc: string) {
233
- const tid = f.translations.value[loc]
234
- if (typeof tid !== 'number') return
235
- try {
236
- const row = await $fetch<Record<string, unknown>>(`/api/${props.collection}/readOne/${tid}`)
237
- f.applyFrom(row)
238
- } catch {
239
- f.formError.value = t('localeBar.copyFailed', { locale: loc.toUpperCase() })
240
- }
241
- }
242
-
243
- // Native unload (tab close / reload / external navigation) bypasses the in-app route guard on the editor
244
- // pages, so a beforeunload prompt guards the same unsaved changes. Lives here (where `dirty` is owned) so
245
- // both the record editor and the singleton editor are covered.
246
- function onBeforeUnload(e: BeforeUnloadEvent) {
247
- if (dirty.value) {
248
- e.preventDefault()
249
- e.returnValue = '' // legacy fallback for the native prompt
250
- }
251
- }
252
- onMounted(() => {
253
- window.addEventListener('beforeunload', onBeforeUnload)
254
- if (pendingPoll.value) {
255
- // Arrived here from a create → the republish is still in flight, so poll until the right lamp settles.
256
- pendingPoll.value = false
257
- void liveStatus.refreshUntilSettled()
258
- } else {
259
- void liveStatus.refresh() // fetch the live/generated state once the saved record (path/locale) is known
260
- }
261
- })
262
- onUnmounted(() => {
263
- window.removeEventListener('beforeunload', onBeforeUnload)
264
- })
265
- </script>
266
-
267
- <template>
268
- <!-- novalidate: Zod owns validation (inline + banner). Native constraint validation would run BEFORE
269
- the submit event and block onSave with a browser bubble — wrongly so on proxy inputs whose text
270
- doesn't mirror the model (combobox search boxes stay empty although records are selected). -->
271
- <form :id="formId" class="editor" novalidate @submit.prevent="onSave">
272
- <!-- Quarantined: the row failed its select schema, so its real fields are unknown — no body,
273
- no actions, nothing to submit. KestrelUiAlert renders role="alert" for variant="error". -->
274
- <KestrelUiAlert v-if="quarantined" variant="error" class="editor__quarantine">
275
- <template #title>
276
- <KestrelUiIcon name="triangle-alert" :size="16" />
277
- {{ t('editor.quarantined.title') }}
278
- </template>
279
- {{ t('editor.quarantined.desc') }}
280
- </KestrelUiAlert>
281
- <template v-else>
282
- <p v-if="formError" class="editor__error" role="alert">{{ formError }}</p>
283
-
284
- <!-- The editor body is chosen by the collection's `editor` type (fields · blocks · an extension). -->
285
- <component :is="bodyComponent" v-if="bodyComponent" />
286
- <KestrelEditorUnsupported v-else :editor="editorType" />
287
-
288
- <div v-if="actions" class="editor__actions">
289
- <KestrelUiButton type="submit" variant="primary" icon="check" :loading="saving">{{ t('common.save') }}</KestrelUiButton>
290
- <KestrelUiButton type="button" variant="secondary" icon="x" :disabled="saving" @click="emit('cancel')">{{ t('common.cancel') }}</KestrelUiButton>
291
- </div>
292
- </template>
293
- </form>
294
- </template>
295
-
296
- <style lang="scss">
297
- // The editor form fills the height handed down by the page so its body/panes can own the scroll.
298
- .editor {
299
- display: flex;
300
- flex-direction: column;
301
- gap: var(--space-5);
302
- flex: 1 1 auto;
303
- min-height: 0;
304
-
305
- &__error {
306
- padding: var(--space-3) var(--space-4);
307
- border: 1px solid var(--color-danger);
308
- border-radius: var(--radius-sm);
309
- color: var(--color-danger);
310
- font-size: var(--text-sm);
311
- }
312
- &__quarantine {
313
- .ui-alert__title {
314
- display: inline-flex;
315
- align-items: center;
316
- gap: var(--space-2);
317
- }
318
- }
319
- &__actions {
320
- display: flex;
321
- gap: var(--space-3);
322
- }
323
- }
324
- </style>