@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,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>