@michaelthielemann/kestrel 4.1.3 → 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,267 +0,0 @@
1
- import { toRaw } from 'vue'
2
- import type { FieldDef, SerializedField } from '@michaelthielemann/kestrel-core'
3
- import { resolveFieldEmpty } from '../../../ui/app/utils/field-empty'
4
-
5
- export interface ServerIssue {
6
- path: (string | number)[]
7
- message: string
8
- code?: string
9
- }
10
-
11
- export interface MappedErrors {
12
- fields: Record<string, string>
13
- form?: string
14
- }
15
-
16
- export interface FetchErrorInfo {
17
- statusCode?: number
18
- statusMessage?: string
19
- issues: ServerIssue[]
20
- /** The error body when it is NOT a Zod-issue array — e.g. `{ savedUpdatedAt }` from a failed write effect. */
21
- data?: Record<string, unknown>
22
- }
23
-
24
- // The wire `SerializedField` is structurally what every Field* widget and `validateField` read
25
- // (flat type + options record + relation); only its discriminated-union typing differs. Narrow here.
26
- export function asFieldDef(field: SerializedField): FieldDef {
27
- return field as unknown as FieldDef
28
- }
29
-
30
- // Read an ofetch/H3 error envelope: top-level statusCode/statusMessage, Zod issues nested at err.data.data.
31
- // The BODY's statusMessage wins over the response's: ofetch maps `statusMessage` from `response.statusText`,
32
- // and HTTP/2 has no reason phrase — behind any h2 proxy that arrives as '' and the real message is only in
33
- // the JSON. Anything else under `data` (not an issue array) is handed back for the caller to act on.
34
- export function readFetchError(e: unknown): FetchErrorInfo {
35
- const err = e as { statusCode?: number; statusMessage?: string; data?: { data?: ServerIssue[] } & Record<string, unknown> }
36
- const body = Array.isArray(err.data) ? undefined : err.data
37
- const raw = body?.data ?? err.data
38
- return {
39
- statusCode: err.statusCode,
40
- statusMessage: (body?.statusMessage as string | undefined) || err.statusMessage,
41
- issues: Array.isArray(raw) ? (raw as ServerIssue[]) : [],
42
- ...(body && !Array.isArray(body.data) ? { data: body } : {}),
43
- }
44
- }
45
-
46
- // `field.default` on a block-picker's field comes from `useBlocks()`'s `useState`-backed list — a Vue
47
- // reactive Proxy for any object/array value. `structuredClone` cannot clone a Proxy (`DataCloneError`,
48
- // deterministic, not a race) — `toRaw` unwraps it first, same fix `cloneBlockTree` reaches for via a JSON
49
- // round-trip; `toRaw` + `structuredClone` keeps real Date/Map values a JSON round-trip would mangle.
50
- function cloneDefault(value: unknown): unknown {
51
- return value !== null && typeof value === 'object' ? structuredClone(toRaw(value)) : value
52
- }
53
-
54
- function emptyForField(field: SerializedField): unknown {
55
- switch (field.type) {
56
- case 'text':
57
- return ''
58
- case 'boolean':
59
- return false
60
- case 'repeater':
61
- return []
62
- case 'choice':
63
- return field.options?.multiple ? [] : null
64
- case 'relation':
65
- return field.relation?.many ? [] : null
66
- case 'media':
67
- return field.options?.multiple ? [] : null
68
- default:
69
- // A consumer-defined type may register its blank shape (array/object-backed → [] / {}); else null.
70
- return resolveFieldEmpty(field.type)?.() ?? null
71
- }
72
- }
73
-
74
- /** Build a blank form state: a present (JSON-safe) default wins, else a type-appropriate empty. */
75
- export function initialValues(fields: Record<string, SerializedField>): Record<string, unknown> {
76
- const out: Record<string, unknown> = {}
77
- for (const [name, field] of Object.entries(fields)) {
78
- out[name] = 'default' in field ? cloneDefault(field.default) : emptyForField(field)
79
- }
80
- return out
81
- }
82
-
83
- /** Map a 400 body's Zod issues to per-field errors (by path[0]) and a single form-level error (empty path). */
84
- export function mapServerErrors(issues: ServerIssue[] | null | undefined): MappedErrors {
85
- const fields: Record<string, string> = {}
86
- let form: string | undefined
87
- for (const issue of issues ?? []) {
88
- const head = issue.path?.[0]
89
- if (head === undefined || head === null || head === '') {
90
- if (form === undefined) form = issue.message
91
- } else {
92
- const name = String(head)
93
- if (!(name in fields)) fields[name] = issue.message
94
- }
95
- }
96
- return form === undefined ? { fields } : { fields, form }
97
- }
98
-
99
- /** Per-block (keyed by the stable `block.id`), per-field error map for inline block-content errors. */
100
- export type BlockErrorMap = Record<string, Record<string, string>>
101
-
102
- interface BlockLike {
103
- id?: unknown
104
- props?: Record<string, unknown>
105
- slots?: Record<string, unknown>
106
- }
107
-
108
- function blockId(block: unknown): string | null {
109
- const id = (block as BlockLike | undefined)?.id
110
- return typeof id === 'string' && id !== '' ? id : null
111
- }
112
-
113
- /**
114
- * Parse nested block-content issues into a per-block, per-field error map keyed by the block's
115
- * stable `id`. The server reports a *position* path — `[key, i, 'props', field, …]` for a top-level
116
- * block, `[key, i, 'slots', name, j, 'props', field, …]` for one nested in a slot (any depth). We
117
- * walk that path through the submitted `content` to find the target block and pin the error to its
118
- * id, so it survives a later reorder at any level. First wins per field; an unresolvable path is
119
- * skipped.
120
- */
121
- export function parseBlockErrors(
122
- issues: ServerIssue[] | null | undefined,
123
- content: readonly unknown[] | null | undefined,
124
- key = 'content',
125
- ): BlockErrorMap {
126
- const out: BlockErrorMap = {}
127
- for (const issue of issues ?? []) {
128
- const path = issue.path ?? []
129
- if (path[0] !== key) continue
130
- const hit = resolveBlockField(path.slice(1), content)
131
- if (!hit) continue
132
- out[hit.id] ??= {}
133
- if (!(hit.field in out[hit.id]!)) out[hit.id]![hit.field] = issue.message
134
- }
135
- return out
136
- }
137
-
138
- /**
139
- * Walk a position path (after the `content` head) through the block tree to the block whose
140
- * `props.<field>` the issue targets. Steps over `<index>, 'slots', <name>` segments into nested
141
- * slot arrays. Returns the target block's stable id + the field name, or null if the path is not a
142
- * `…props.<field>` leaf or any segment doesn't resolve to an identified block.
143
- */
144
- function resolveBlockField(
145
- path: readonly (string | number)[],
146
- blocks: readonly unknown[] | null | undefined,
147
- ): { id: string; field: string } | null {
148
- let arr: readonly unknown[] | null | undefined = blocks
149
- for (let i = 0; i < path.length; ) {
150
- const index = path[i]
151
- if (typeof index !== 'number' || !Array.isArray(arr)) return null
152
- const block = arr[index] as BlockLike | undefined
153
- const next = path[i + 1]
154
- if (next === 'props') {
155
- const field = path[i + 2]
156
- const id = blockId(block)
157
- return typeof field === 'string' && id !== null ? { id, field } : null
158
- }
159
- if (next === 'slots') {
160
- const name = path[i + 2]
161
- if (typeof name !== 'string') return null
162
- const slot = block?.slots?.[name]
163
- arr = Array.isArray(slot) ? slot : null
164
- i += 3
165
- continue
166
- }
167
- return null
168
- }
169
- return null
170
- }
171
-
172
- /**
173
- * Carry block errors across a `content` change. A block (identified by its stable `id`, top-level or nested in
174
- * a slot at any depth) keeps its errors while it merely moves — a pure reorder, whether of its slot or of an
175
- * ancestor — and is dropped when removed; added blocks never gain an error. Reconciliation is PER-FIELD: an
176
- * edit clears only the edited field's stale message, so a still-invalid sibling field of the same block keeps
177
- * its error instead of all of them flickering away until the next save.
178
- */
179
- export function reconcileBlockErrors(
180
- errors: BlockErrorMap,
181
- prev: readonly unknown[] | null | undefined,
182
- next: readonly unknown[] | null | undefined,
183
- ): BlockErrorMap {
184
- const prevById = new Map<string, BlockLike>()
185
- const nextById = new Map<string, BlockLike>()
186
- flattenBlocksById(prev, prevById)
187
- flattenBlocksById(next, nextById)
188
- const out: BlockErrorMap = {}
189
- for (const [id, errs] of Object.entries(errors)) {
190
- const before = prevById.get(id)
191
- const after = nextById.get(id)
192
- if (!before || !after) continue // removed → drop
193
- const beforeProps = (before.props ?? {}) as Record<string, unknown>
194
- const afterProps = (after.props ?? {}) as Record<string, unknown>
195
- const kept: Record<string, string> = {}
196
- for (const [field, message] of Object.entries(errs)) {
197
- // Keep a field's error only while THAT field's value is unchanged; editing it clears its own stale message.
198
- if (valuesEqual(beforeProps[field], afterProps[field])) kept[field] = message
199
- }
200
- if (Object.keys(kept).length) out[id] = kept
201
- }
202
- return out
203
- }
204
-
205
- /** Index every block in the tree (top-level and nested in slots) by its stable id. */
206
- function flattenBlocksById(blocks: readonly unknown[] | null | undefined, into: Map<string, BlockLike>): void {
207
- for (const block of blocks ?? []) {
208
- const id = blockId(block)
209
- if (id !== null) into.set(id, block as BlockLike)
210
- const slots = (block as BlockLike | undefined)?.slots
211
- if (slots) for (const arr of Object.values(slots)) if (Array.isArray(arr)) flattenBlocksById(arr, into)
212
- }
213
- }
214
-
215
- /** One stale reference a record holds. Wire shape of `DeadRef` (kept structural here so this module stays
216
- * Vue-free and doesn't import the admin-only `dead-refs.ts` type file). */
217
- interface DeadRefLike {
218
- field: string
219
- blockId?: string
220
- }
221
-
222
- /**
223
- * Carry dead-reference warnings across a `content` change, mirroring `reconcileBlockErrors`: a warning at
224
- * a block (by stable id) is dropped once THAT field's value changes (the user swapped the broken
225
- * reference) or the block is removed; it survives a pure reorder. Root-level (non-block) dead refs are
226
- * untouched here — `setField` drops those directly when their own field is edited.
227
- */
228
- export function reconcileDeadRefs<T extends DeadRefLike>(
229
- refs: readonly T[],
230
- prev: readonly unknown[] | null | undefined,
231
- next: readonly unknown[] | null | undefined,
232
- ): T[] {
233
- const prevById = new Map<string, BlockLike>()
234
- const nextById = new Map<string, BlockLike>()
235
- flattenBlocksById(prev, prevById)
236
- flattenBlocksById(next, nextById)
237
- return refs.filter((r) => {
238
- if (!r.blockId) return true
239
- const before = prevById.get(r.blockId)
240
- const after = nextById.get(r.blockId)
241
- if (!before || !after) return false
242
- const beforeVal = (before.props ?? {})[r.field]
243
- const afterVal = (after.props ?? {})[r.field]
244
- return valuesEqual(beforeVal, afterVal)
245
- })
246
- }
247
-
248
- /** Structural deep-equality for the value shapes the editor tracks (primitives, arrays, plain objects). */
249
- export function valuesEqual(a: unknown, b: unknown): boolean {
250
- if (a === b) return true
251
- if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false
252
-
253
- const aArr = Array.isArray(a)
254
- const bArr = Array.isArray(b)
255
- if (aArr !== bArr) return false
256
- if (aArr && bArr) {
257
- if (a.length !== b.length) return false
258
- return a.every((item, i) => valuesEqual(item, b[i]))
259
- }
260
-
261
- const aRec = a as Record<string, unknown>
262
- const bRec = b as Record<string, unknown>
263
- const aKeys = Object.keys(aRec)
264
- const bKeys = Object.keys(bRec)
265
- if (aKeys.length !== bKeys.length) return false
266
- return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bRec, k) && valuesEqual(aRec[k], bRec[k]))
267
- }
@@ -1,36 +0,0 @@
1
- import type { PublishStatusData } from '../composables/usePublishStatus'
2
-
3
- /**
4
- * The handle `CollectionEditor` exposes via `defineExpose` — the header hosts (`[collection]/[id].vue`
5
- * and `SingletonEditor.vue`) drive Save / undo / preview and read the status ampel through it. ONE typed
6
- * contract instead of three drifting inline copies: Vue does not type-check a template `ref="editorRef"`
7
- * against `defineExpose`, so without a shared interface a renamed/removed key (e.g. `previewUrl`) compiles
8
- * in both hosts and silently becomes `undefined` at runtime. Type both host refs as `EditorExpose | null`.
9
- */
10
- export interface EditorExpose {
11
- dirty: boolean
12
- saving: boolean
13
- canUndo: boolean
14
- canRedo: boolean
15
- undo: () => void
16
- redo: () => void
17
- hasStatus: boolean
18
- status: string
19
- /** The last SAVED publish status (baseline), for the right lamp's draft/published decision. */
20
- savedStatus: string
21
- previewUrl: string | null
22
- pageLike: boolean
23
- live: PublishStatusData | null
24
- /** The record's own title (see `recordTitle`), or `''` — the header then shows "Edit {collection} #{id}". */
25
- recordTitle: string
26
- /** Save, then write the static output (ADR-0008) — a draft is promoted to published on the way. */
27
- publish: () => Promise<void>
28
- publishing: boolean
29
- /** False when `output.publishOnSave` is on — a save republishes, so the hosts hide the Publish button. */
30
- canPublish: boolean
31
- /** Open the record in a new tab: the saved URL, or the unsaved state carried by a preview ticket. */
32
- openPreview: () => Promise<void>
33
- previewOpening: boolean
34
- /** The loaded row is the quarantine shape — read-only, hosts must hide Save/Publish. */
35
- quarantined: boolean
36
- }
@@ -1,82 +0,0 @@
1
- import type { ComputedRef, InjectionKey, Ref } from 'vue'
2
- import type { FieldDef, LayoutNode } from '@michaelthielemann/kestrel-core'
3
- import type { BlockErrorMap } from './edit-form'
4
- import type { DeadRef } from './dead-refs'
5
-
6
- /**
7
- * The form context `CollectionEditor` (the generic shell) provides to whichever editor **body** the
8
- * registry resolves (fields · blocks · an extension's, e.g. node-graph). It carries the load-bearing
9
- * `useEditForm` surface a body needs to read and mutate the record, so a body swaps in without a giant
10
- * prop list — the same provide/inject seam as `recordEditContextKey`, one level up.
11
- *
12
- * Provide it BEFORE the shell's top-level `await` (after an await the setup instance is gone and `provide`
13
- * silently no-ops), exactly like the record-edit context.
14
- */
15
- /** Props for the shared page-fields pane. Defined once here so the shell's pre-built bindings and
16
- * `PageFieldsPane`'s own `defineProps` stay a single type (a widened `Record` would break the v-bind). */
17
- export interface PageFieldsBindings {
18
- translatable?: boolean
19
- collection: string
20
- id: string
21
- mode: 'single' | 'multi'
22
- locale: string
23
- translations: Record<string, number | null>
24
- group?: string
25
- fields: Record<string, FieldDef>
26
- /** Normalized admin editor layout for the page fields (undefined → one field per row). */
27
- fieldLayout?: LayoutNode[]
28
- values: Record<string, unknown>
29
- errors: Record<string, string>
30
- deadFields?: Set<string>
31
- pageLike?: boolean
32
- seo?: boolean
33
- status?: boolean
34
- disabled?: boolean
35
- }
36
-
37
- export interface EditorFormContext {
38
- /** Reactive form values / per-field errors (from `useEditForm`; reactive objects, not refs). */
39
- values: Record<string, unknown>
40
- errors: Record<string, string>
41
- formError: Ref<string>
42
- blockErrors: Ref<BlockErrorMap>
43
- /** Route every mutation through this (→ block-error reconciliation, dirty tracking, history). The
44
- * optional `coalesceAs` lets a caller (e.g. a block-tree structural op) tag its own undo-history key
45
- * instead of coalescing under the bare field name. */
46
- setField: (name: string, value: unknown, coalesceAs?: string) => void
47
- locale: Ref<string>
48
- translations: Ref<Record<string, number | null>>
49
- translationGroup: Ref<string | undefined>
50
- deadRefs: Ref<DeadRef[]>
51
- saving: Ref<boolean>
52
- mode: Ref<'multi' | 'single'>
53
- translatable: Ref<boolean>
54
- pageLike: Ref<boolean>
55
- hasSeo: Ref<boolean>
56
- hasStatus: Ref<boolean>
57
- blocksAllowed: Ref<string[] | undefined>
58
- /** Collection (page-root) fields as `FieldDef`s, ready for the field Renderer. */
59
- renderable: ComputedRef<Record<string, FieldDef>>
60
- /** The record's localized public URL (saved pageLike record with a path), or null (new/unsaved,
61
- * non-pageLike). The shell computes it once for the header's open-in-new-tab AND the live preview. */
62
- previewUrl: ComputedRef<string | null>
63
- undo: () => void
64
- redo: () => void
65
- applyFrom: (row: Record<string, unknown>) => void
66
- /** Pre-bound props/handlers for the shared page-fields pane, rendered by both the fields and blocks
67
- * bodies (identical binding, differing only in layout chrome). */
68
- pageFieldsBindings: ComputedRef<PageFieldsBindings>
69
- pageFieldsHandlers: Record<string, (...args: never[]) => void>
70
- /** A body registers how to reveal a validation failure (e.g. the blocks body focuses the offending
71
- * block). The shell calls it on a failed save, staying agnostic of the body's internals. */
72
- registerRevealError: (fn: () => void) => void
73
- }
74
-
75
- export const editorFormContextKey = Symbol('kestrel.editorForm') as InjectionKey<EditorFormContext>
76
-
77
- /** Inject the editor form context inside a body component; throws if used outside `CollectionEditor`. */
78
- export function useEditorFormContext(): EditorFormContext {
79
- const ctx = inject(editorFormContextKey)
80
- if (!ctx) throw new Error('[kestrel] editor body used outside CollectionEditor (no editor form context)')
81
- return ctx
82
- }
@@ -1,20 +0,0 @@
1
- import type { Component } from 'vue'
2
-
3
- /**
4
- * Registry of admin editor *bodies* keyed by editor type (the `editor` field on a serialized collection).
5
- * Mirrors the field-widget registry (`layers/ui/app/utils/field-registry.ts`), one level up: instead of
6
- * picking a widget per field type, it picks the whole editor body per collection.
7
- *
8
- * The built-in `fields` / `blocks` bodies register themselves when the admin editor loads (see
9
- * `register-editors`); extensions add their own via a `*.client.ts` plugin (auto-discovered through
10
- * `extends`), exactly like `registerFieldComponent`. The frame — the admin rail (layout) and the record
11
- * header/toolbar + form state (`CollectionEditor` / the host pages) — stays generic; only this body swaps.
12
- */
13
- export const editorComponents: Record<string, Component> = {}
14
-
15
- export const resolveCollectionEditor = (type: string): Component | undefined => editorComponents[type]
16
-
17
- /** Register an editor body for a type. Mutates the singleton; a later call overrides the same type. */
18
- export function registerCollectionEditor(type: string, component: Component): void {
19
- editorComponents[type] = component
20
- }
@@ -1,11 +0,0 @@
1
- import type { SerializedField } from '@michaelthielemann/kestrel-core'
2
-
3
- /**
4
- * The jsKey (wire key) for a field — NOT a DB column name. The server marks single relation/media fields
5
- * with `single` (stored as a `<name>Id` FK column, e.g. `author` -> `authorId`); everything else keeps the
6
- * field name. The editor binds widgets by field name but must read/write rows, request bodies and list
7
- * queries by this key — reading the server flag instead of re-deriving the rule (which silently drifts).
8
- */
9
- export function jsKey(name: string, field: SerializedField): string {
10
- return field.single ? `${name}Id` : name
11
- }
@@ -1,46 +0,0 @@
1
- // Cell rendering for the collection list: how a raw row value becomes the string in a table cell.
2
- import type { ListColumn } from './list-columns'
3
- import { humanizeFieldName } from '../../../ui/app/utils/humanize'
4
-
5
- const dateFmt = new Intl.DateTimeFormat(undefined, { year: 'numeric', month: 'short', day: '2-digit' })
6
- // A date-only value ('YYYY-MM-DD', from a datetime field with precision 'date') parses as UTC midnight;
7
- // formatting it in the local zone would shift the calendar day, so pin it to UTC. Full ISO timestamps
8
- // (createdAt/updatedAt carry a 'Z') and local datetimes keep the local-zone formatter.
9
- const dateOnlyFmt = new Intl.DateTimeFormat(undefined, { year: 'numeric', month: 'short', day: '2-digit', timeZone: 'UTC' })
10
-
11
- export function isDateColumn(col: ListColumn): boolean {
12
- return col.key === 'createdAt' || col.key === 'updatedAt' || col.field?.type === 'datetime'
13
- }
14
-
15
- export function cellText(value: unknown): string {
16
- if (value == null) return ''
17
- if (typeof value === 'string') return value.replace(/<[^>]*>/g, '').slice(0, 80)
18
- if (typeof value === 'object') return JSON.stringify(value).slice(0, 80)
19
- return String(value)
20
- }
21
-
22
- export function cellDisplay(col: ListColumn, row: Record<string, unknown>): string {
23
- const value = row[col.key]
24
- if (isDateColumn(col) && value != null) {
25
- const dateOnly = typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)
26
- const d = new Date(value as string)
27
- if (!Number.isNaN(d.getTime())) return (dateOnly ? dateOnlyFmt : dateFmt).format(d)
28
- }
29
- return cellText(value)
30
- }
31
-
32
- /** A column header's text: a field column humanizes its own name, chrome columns translate a key. */
33
- export function columnLabel(col: ListColumn, t: (key: string) => string): string {
34
- return col.type === 'field' ? humanizeFieldName(col.name ?? col.key) : t(col.labelKey ?? col.key)
35
- }
36
-
37
- /** A row's human label: the first visible non-sidecar cell that has a value, else the bare id — used for
38
- * the per-row checkbox / action aria-labels so each control names its row. */
39
- export function rowLabel(columns: ListColumn[], row: Record<string, unknown>): string {
40
- for (const c of columns) {
41
- if (c.type === 'translations' || c.type === 'deadRefs') continue
42
- const d = cellDisplay(c, row)
43
- if (d) return d
44
- }
45
- return `#${row.id}`
46
- }
@@ -1,142 +0,0 @@
1
- import { META_FILTER_KIND, fieldFilterKind } from '@michaelthielemann/kestrel-core/client'
2
- import type { FilterKind } from '@michaelthielemann/kestrel-core/client'
3
- import type { SerializedCollection, SerializedField } from '@michaelthielemann/kestrel-core'
4
- import { jsKey } from './field-keys'
5
-
6
- export type ListColumnType = 'meta' | 'field' | 'translations' | 'deadRefs'
7
-
8
- export interface ListColumn {
9
- /** The wire/row key: used to read `row[key]` and as the sort/filter field. The translations column
10
- * uses the server sidecar key `$translations`, which is never sent as a sort/filter (not a real column). */
11
- key: string
12
- type: ListColumnType
13
- /** Field display name (only for `type:'field'`) — used directly as the column label. */
14
- name?: string
15
- /** i18n key for the column label (meta + translations columns). */
16
- labelKey?: string
17
- field?: SerializedField
18
- sortable: boolean
19
- filterable: boolean
20
- /** The column's filter kind (present iff `filterable`) — drives the operator set + typed value control. */
21
- filterKind?: FilterKind
22
- }
23
-
24
- /** The sidecar key the server attaches per row for translation status (mirrors `$media`). */
25
- export const TRANSLATIONS_KEY = '$translations'
26
-
27
- /** The sidecar key the server attaches per row when any reference it holds points at a dead/unpublished
28
- * target (the derived stale-reference warning). Like `$translations`, never a real sort/filter column. */
29
- export const DEAD_REFS_KEY = '$hasDeadRefs'
30
-
31
- /** Whether a collection holds any field/block that can reference another record — gates the dead-reference
32
- * column (mirrors the server's `collectionMayReference`, over the serialized schema). */
33
- export function hasReferenceFields(schema: SerializedCollection): boolean {
34
- if (schema.blocks.enabled) return true
35
- if (schema.seo) return true // the `seo` system column carries a media ref (the social image)
36
- return Object.values(schema.fields).some(fieldMayReference)
37
- }
38
- function fieldMayReference(field: SerializedField): boolean {
39
- switch (field.type) {
40
- case 'relation':
41
- case 'media':
42
- case 'link':
43
- case 'richtext':
44
- return true
45
- case 'repeater':
46
- return Object.values((field.options?.fields ?? {}) as Record<string, SerializedField>).some(fieldMayReference)
47
- default:
48
- return false
49
- }
50
- }
51
-
52
- const META_LABEL: Record<string, string> = {
53
- id: 'list.col.id',
54
- path: 'list.col.slug',
55
- status: 'list.col.status',
56
- createdAt: 'list.col.createdAt',
57
- updatedAt: 'list.col.updatedAt',
58
- }
59
-
60
- /** Whether a field can be meaningfully SORTED as a single scalar DB column. JSON-backed fields
61
- * (richtext/json/repeater/link, multi-value choice/relation/media) cannot. (Filterability is a separate,
62
- * richer question — see `fieldFilterKind`: richtext and the multi-value fields ARE filterable, via
63
- * substring / JSON-array membership, even though they are not sortable.) */
64
- function isSortableField(f: SerializedField): boolean {
65
- switch (f.type) {
66
- case 'richtext':
67
- case 'json':
68
- case 'repeater':
69
- case 'link':
70
- return false
71
- case 'choice':
72
- return !f.options?.multiple
73
- case 'relation':
74
- case 'media':
75
- return !!f.single
76
- default:
77
- return true // text, number, boolean, datetime
78
- }
79
- }
80
-
81
- /** Whether a field column is shown by default — only plain scalar display types. Heavier/structured fields
82
- * (richtext, json, repeater, link, relation, media, multi-choice) are available but off by default. */
83
- function isDefaultFieldColumn(f: SerializedField): boolean {
84
- switch (f.type) {
85
- case 'text':
86
- case 'number':
87
- case 'boolean':
88
- case 'datetime':
89
- return true
90
- case 'choice':
91
- return !f.options?.multiple
92
- default:
93
- return false
94
- }
95
- }
96
-
97
- /** A meta/system column, filterable iff it has a FilterKind (id/path/status/timestamps all do). */
98
- function metaColumn(key: string): ListColumn {
99
- const filterKind = META_FILTER_KIND[key]
100
- return { key, type: 'meta', labelKey: META_LABEL[key], sortable: true, filterable: !!filterKind, ...(filterKind ? { filterKind } : {}) }
101
- }
102
-
103
- /** Every column a collection list can offer, in display order: id · fields · slug · status · timestamps ·
104
- * translations. The user picks which are visible (see `defaultVisibleKeys` / `resolveVisibleColumns`). */
105
- export function availableColumns(schema: SerializedCollection): ListColumn[] {
106
- const cols: ListColumn[] = [metaColumn('id')]
107
- for (const [name, field] of Object.entries(schema.fields)) {
108
- const filterKind = fieldFilterKind(field)
109
- cols.push({ key: jsKey(name, field), type: 'field', name, field, sortable: isSortableField(field), filterable: filterKind !== null, ...(filterKind ? { filterKind } : {}) })
110
- }
111
- if (schema.pageLike) cols.push(metaColumn('path'))
112
- if (schema.status) cols.push(metaColumn('status'))
113
- cols.push(metaColumn('createdAt'))
114
- cols.push(metaColumn('updatedAt'))
115
- if (schema.translatable && schema.mode === 'multi') {
116
- cols.push({ key: TRANSLATIONS_KEY, type: 'translations', labelKey: 'list.translations', sortable: false, filterable: false })
117
- }
118
- // A compact warning column (a triangle on rows holding a stale reference) — only where refs are possible.
119
- if (hasReferenceFields(schema)) {
120
- cols.push({ key: DEAD_REFS_KEY, type: 'deadRefs', labelKey: 'list.deadRefs', sortable: false, filterable: false })
121
- }
122
- return cols
123
- }
124
-
125
- /** The default visible set: everything except `id` and the heavier field types — i.e. the scalar fields
126
- * plus slug, status, timestamps and translations. */
127
- export function defaultVisibleKeys(schema: SerializedCollection): string[] {
128
- return availableColumns(schema)
129
- .filter((c) => {
130
- if (c.key === 'id') return false
131
- if (c.type === 'field') return isDefaultFieldColumn(c.field!)
132
- return true
133
- })
134
- .map((c) => c.key)
135
- }
136
-
137
- /** Project the requested keys onto the available columns: preserves the canonical column order and silently
138
- * drops keys that no longer exist (e.g. a removed field still listed in a saved preference). */
139
- export function resolveVisibleColumns(available: ListColumn[], keys: string[]): ListColumn[] {
140
- const wanted = new Set(keys)
141
- return available.filter((c) => wanted.has(c.key))
142
- }