@elytracms/studio 0.0.93 → 0.0.94

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 (481) hide show
  1. package/convex/analytics-cost.test.ts +817 -0
  2. package/convex/auth.ts +5 -2
  3. package/convex/authority.ts +35 -11
  4. package/convex/contract-parity.test.ts +7 -0
  5. package/convex/delivery.ts +12 -9
  6. package/convex/guard.ts +9 -7
  7. package/convex/http-routes.test.ts +18 -8
  8. package/convex/http.ts +23 -17
  9. package/convex/ingest.test.ts +326 -0
  10. package/convex/ingest.ts +281 -0
  11. package/convex/invites.ts +13 -5
  12. package/convex/members.ts +10 -4
  13. package/convex/record-functions.ts +325 -0
  14. package/convex/records.test.ts +156 -0
  15. package/convex/redirect-migration.test.ts +205 -0
  16. package/convex/redirect-revalidate.test.ts +5 -1
  17. package/convex/redirects.ts +120 -79
  18. package/convex/role-policy.test.ts +315 -0
  19. package/convex/roles.ts +83 -0
  20. package/convex/schema.ts +30 -0
  21. package/convex/triggers.ts +21 -1
  22. package/dist/addon.d.ts +71 -0
  23. package/dist/addon.js +67 -0
  24. package/dist/addon.js.map +1 -0
  25. package/dist/components/account/account-settings.js +2 -2
  26. package/dist/components/account/account-settings.js.map +1 -1
  27. package/dist/components/account/credential-panel.js +2 -2
  28. package/dist/components/account/credential-panel.js.map +1 -1
  29. package/dist/components/account/self-account-schema.d.ts +6 -15
  30. package/dist/components/account/self-account-schema.js +52 -34
  31. package/dist/components/account/self-account-schema.js.map +1 -1
  32. package/dist/components/account/self-account-view-source.d.ts +1 -1
  33. package/dist/components/account/self-account-view-source.js +6 -4
  34. package/dist/components/account/self-account-view-source.js.map +1 -1
  35. package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
  36. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  37. package/dist/components/ai/ai-chat-panel.js +6 -6
  38. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  39. package/dist/components/ai/ai-cms-panel.js +2 -2
  40. package/dist/components/ai/ai-cms-panel.js.map +1 -1
  41. package/dist/components/ai/ai-panel-toggle.js +4 -4
  42. package/dist/components/ai/ai-panel-toggle.js.map +1 -1
  43. package/dist/components/ai/chat-ui.js +2 -2
  44. package/dist/components/ai/chat-ui.js.map +1 -1
  45. package/dist/components/ai/proposal-card.d.ts +2 -2
  46. package/dist/components/ai/proposal-card.js +2 -2
  47. package/dist/components/ai/proposal-card.js.map +1 -1
  48. package/dist/components/ai/proposal-issues.js +2 -2
  49. package/dist/components/ai/proposal-issues.js.map +1 -1
  50. package/dist/components/ai/translate-document-action.js +2 -2
  51. package/dist/components/ai/translate-document-action.js.map +1 -1
  52. package/dist/components/ai/translate-document-dialog.js +6 -6
  53. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  54. package/dist/components/ai/translate-skipped.d.ts +1 -1
  55. package/dist/components/ai/translate-skipped.js +2 -2
  56. package/dist/components/ai/translate-skipped.js.map +1 -1
  57. package/dist/components/app-views/default-modules.d.ts +50 -2
  58. package/dist/components/app-views/default-modules.js +65 -20
  59. package/dist/components/app-views/default-modules.js.map +1 -1
  60. package/dist/components/app-views/module.d.ts +53 -3
  61. package/dist/components/app-views/module.js +14 -7
  62. package/dist/components/app-views/module.js.map +1 -1
  63. package/dist/components/app-views/sidebar.js +1 -1
  64. package/dist/components/app-views/sidebar.js.map +1 -1
  65. package/dist/components/app-views/studio-config.d.ts +11 -2
  66. package/dist/components/app-views/studio-config.js.map +1 -1
  67. package/dist/components/assets/asset-detail.js +2 -2
  68. package/dist/components/assets/asset-detail.js.map +1 -1
  69. package/dist/components/assets/asset-grid.js +2 -2
  70. package/dist/components/assets/asset-grid.js.map +1 -1
  71. package/dist/components/assets/asset-thumb.js +3 -3
  72. package/dist/components/assets/asset-thumb.js.map +1 -1
  73. package/dist/components/assets/assets-collection-view-source.d.ts +2 -2
  74. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  75. package/dist/components/assets/assets-module.js +1 -1
  76. package/dist/components/assets/assets-module.js.map +1 -1
  77. package/dist/components/assets/assets-store.d.ts +2 -2
  78. package/dist/components/assets/assets-store.js +6 -6
  79. package/dist/components/assets/assets-store.js.map +1 -1
  80. package/dist/components/assets/assets-view-preference.js +1 -1
  81. package/dist/components/assets/assets-view-preference.js.map +1 -1
  82. package/dist/components/assets/replace-file.js +1 -1
  83. package/dist/components/assets/replace-file.js.map +1 -1
  84. package/dist/components/auth-context.d.ts +1 -1
  85. package/dist/components/auth-context.js +6 -6
  86. package/dist/components/auth-context.js.map +1 -1
  87. package/dist/components/builder/blocks-model.d.ts +1 -1
  88. package/dist/components/builder/blocks-model.js +3 -3
  89. package/dist/components/builder/blocks-model.js.map +1 -1
  90. package/dist/components/builder/blocks-panel.d.ts +1 -1
  91. package/dist/components/builder/blocks-panel.js +4 -4
  92. package/dist/components/builder/blocks-panel.js.map +1 -1
  93. package/dist/components/builder/builder-store.js +2 -2
  94. package/dist/components/builder/builder-store.js.map +1 -1
  95. package/dist/components/builder/canvas-breakpoints.js +1 -1
  96. package/dist/components/builder/canvas-breakpoints.js.map +1 -1
  97. package/dist/components/builder/canvas-dnd.d.ts +2 -2
  98. package/dist/components/builder/canvas-dnd.js +1 -1
  99. package/dist/components/builder/canvas-dnd.js.map +1 -1
  100. package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
  101. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  102. package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
  103. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
  104. package/dist/components/builder/canvas-render-context.d.ts +1 -1
  105. package/dist/components/builder/canvas-render-context.js +4 -4
  106. package/dist/components/builder/canvas-render-context.js.map +1 -1
  107. package/dist/components/builder/canvas-theme-notice.js +2 -2
  108. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  109. package/dist/components/builder/canvas.js +3 -3
  110. package/dist/components/builder/canvas.js.map +1 -1
  111. package/dist/components/builder/clipboard-actions.d.ts +2 -2
  112. package/dist/components/builder/clipboard-actions.js +4 -4
  113. package/dist/components/builder/clipboard-actions.js.map +1 -1
  114. package/dist/components/builder/composition-field-editor-lazy.js +1 -1
  115. package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
  116. package/dist/components/builder/composition-field-editor.d.ts +1 -1
  117. package/dist/components/builder/composition-field-editor.js +3 -3
  118. package/dist/components/builder/composition-field-editor.js.map +1 -1
  119. package/dist/components/builder/drag-session.d.ts +1 -1
  120. package/dist/components/builder/drag-session.js.map +1 -1
  121. package/dist/components/builder/fixture-thumbnail.js +1 -1
  122. package/dist/components/builder/fixture-thumbnail.js.map +1 -1
  123. package/dist/components/builder/insert-context.d.ts +1 -1
  124. package/dist/components/builder/insert-context.js +3 -3
  125. package/dist/components/builder/insert-context.js.map +1 -1
  126. package/dist/components/builder/layout-inspector.js +1 -1
  127. package/dist/components/builder/layout-inspector.js.map +1 -1
  128. package/dist/components/builder/node-title.js +1 -1
  129. package/dist/components/builder/node-title.js.map +1 -1
  130. package/dist/components/builder/page-tree.d.ts +1 -1
  131. package/dist/components/builder/page-tree.js +5 -5
  132. package/dist/components/builder/page-tree.js.map +1 -1
  133. package/dist/components/builder/placement-refusal.js +3 -3
  134. package/dist/components/builder/placement-refusal.js.map +1 -1
  135. package/dist/components/builder/property-inspector.js +4 -4
  136. package/dist/components/builder/property-inspector.js.map +1 -1
  137. package/dist/components/builder/reuse-context.js +4 -4
  138. package/dist/components/builder/reuse-context.js.map +1 -1
  139. package/dist/components/builder/reuse.js +1 -1
  140. package/dist/components/builder/reuse.js.map +1 -1
  141. package/dist/components/builder/rich-text-toolbar.js +1 -1
  142. package/dist/components/builder/rich-text-toolbar.js.map +1 -1
  143. package/dist/components/builder/run-inspector.js +1 -1
  144. package/dist/components/builder/run-inspector.js.map +1 -1
  145. package/dist/components/builder/selection-overlay.js +3 -3
  146. package/dist/components/builder/selection-overlay.js.map +1 -1
  147. package/dist/components/builder/spacing-handles.d.ts +2 -2
  148. package/dist/components/builder/spacing-handles.js +3 -3
  149. package/dist/components/builder/spacing-handles.js.map +1 -1
  150. package/dist/components/cms/asset-field.js +2 -2
  151. package/dist/components/cms/asset-field.js.map +1 -1
  152. package/dist/components/cms/asset-picker.js +2 -2
  153. package/dist/components/cms/asset-picker.js.map +1 -1
  154. package/dist/components/cms/bulk-actions.d.ts +1 -1
  155. package/dist/components/cms/bulk-actions.js +2 -2
  156. package/dist/components/cms/bulk-actions.js.map +1 -1
  157. package/dist/components/cms/bulk-bar.d.ts +2 -2
  158. package/dist/components/cms/bulk-bar.js +2 -2
  159. package/dist/components/cms/bulk-bar.js.map +1 -1
  160. package/dist/components/cms/cell-edit.d.ts +1 -1
  161. package/dist/components/cms/cell-edit.js +1 -1
  162. package/dist/components/cms/cell-edit.js.map +1 -1
  163. package/dist/components/cms/cell.d.ts +1 -1
  164. package/dist/components/cms/cell.js +3 -3
  165. package/dist/components/cms/cell.js.map +1 -1
  166. package/dist/components/cms/cms-index.js +2 -2
  167. package/dist/components/cms/cms-index.js.map +1 -1
  168. package/dist/components/cms/cms-labels.js +2 -2
  169. package/dist/components/cms/cms-labels.js.map +1 -1
  170. package/dist/components/cms/cms-store.d.ts +2 -2
  171. package/dist/components/cms/cms-store.js +10 -10
  172. package/dist/components/cms/cms-store.js.map +1 -1
  173. package/dist/components/cms/collection-table.js +7 -7
  174. package/dist/components/cms/collection-table.js.map +1 -1
  175. package/dist/components/cms/detail-editor.d.ts +5 -2
  176. package/dist/components/cms/detail-editor.js +20 -14
  177. package/dist/components/cms/detail-editor.js.map +1 -1
  178. package/dist/components/cms/details-pane.js +1 -1
  179. package/dist/components/cms/details-pane.js.map +1 -1
  180. package/dist/components/cms/doc-state.js +1 -1
  181. package/dist/components/cms/doc-state.js.map +1 -1
  182. package/dist/components/cms/document-diff-view.js +5 -5
  183. package/dist/components/cms/document-diff-view.js.map +1 -1
  184. package/dist/components/cms/document-editor-context.js +1 -1
  185. package/dist/components/cms/document-editor-context.js.map +1 -1
  186. package/dist/components/cms/document-staging.js +1 -1
  187. package/dist/components/cms/document-staging.js.map +1 -1
  188. package/dist/components/cms/document-url-path.js +2 -2
  189. package/dist/components/cms/document-url-path.js.map +1 -1
  190. package/dist/components/cms/duplicate-document.d.ts +1 -1
  191. package/dist/components/cms/duplicate-document.js.map +1 -1
  192. package/dist/components/cms/editable-cell.js +3 -3
  193. package/dist/components/cms/editable-cell.js.map +1 -1
  194. package/dist/components/cms/field-collaboration.d.ts +1 -1
  195. package/dist/components/cms/field-collaboration.js +1 -1
  196. package/dist/components/cms/field-collaboration.js.map +1 -1
  197. package/dist/components/cms/field-connection.d.ts +1 -1
  198. package/dist/components/cms/field-connection.js +2 -2
  199. package/dist/components/cms/field-connection.js.map +1 -1
  200. package/dist/components/cms/field-control-lib.js +1 -1
  201. package/dist/components/cms/field-control-lib.js.map +1 -1
  202. package/dist/components/cms/field-control.js +4 -4
  203. package/dist/components/cms/field-control.js.map +1 -1
  204. package/dist/components/cms/field-renderer.d.ts +1 -1
  205. package/dist/components/cms/field-renderer.js +2 -2
  206. package/dist/components/cms/field-renderer.js.map +1 -1
  207. package/dist/components/cms/filter-builder.d.ts +1 -1
  208. package/dist/components/cms/filter-builder.js +3 -3
  209. package/dist/components/cms/filter-builder.js.map +1 -1
  210. package/dist/components/cms/gate-validation.js +1 -1
  211. package/dist/components/cms/gate-validation.js.map +1 -1
  212. package/dist/components/cms/import-export.js +2 -2
  213. package/dist/components/cms/import-export.js.map +1 -1
  214. package/dist/components/cms/issue-copy.js +1 -1
  215. package/dist/components/cms/issue-copy.js.map +1 -1
  216. package/dist/components/cms/issues-pane.js +3 -3
  217. package/dist/components/cms/issues-pane.js.map +1 -1
  218. package/dist/components/cms/object-array-field.js +2 -2
  219. package/dist/components/cms/object-array-field.js.map +1 -1
  220. package/dist/components/cms/prop-fields.js +2 -2
  221. package/dist/components/cms/prop-fields.js.map +1 -1
  222. package/dist/components/cms/reference-confirm.d.ts +1 -1
  223. package/dist/components/cms/reference-confirm.js +2 -2
  224. package/dist/components/cms/reference-confirm.js.map +1 -1
  225. package/dist/components/cms/relation-picker.js +2 -2
  226. package/dist/components/cms/relation-picker.js.map +1 -1
  227. package/dist/components/cms/rich-text/block-style.js +1 -1
  228. package/dist/components/cms/rich-text/block-style.js.map +1 -1
  229. package/dist/components/cms/rich-text/component-embed.js +4 -4
  230. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  231. package/dist/components/cms/rich-text/editor.js +3 -3
  232. package/dist/components/cms/rich-text/editor.js.map +1 -1
  233. package/dist/components/cms/rich-text/embed-picker.js +3 -3
  234. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  235. package/dist/components/cms/rich-text/embed-props.js +3 -3
  236. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  237. package/dist/components/cms/rich-text/extensions.js +1 -1
  238. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  239. package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
  240. package/dist/components/cms/rich-text/font-size-control.js +2 -2
  241. package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
  242. package/dist/components/cms/rich-text/lazy.js +1 -1
  243. package/dist/components/cms/rich-text/lazy.js.map +1 -1
  244. package/dist/components/cms/rich-text/link-control.js +2 -2
  245. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  246. package/dist/components/cms/rich-text/slash-command.js +3 -3
  247. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  248. package/dist/components/cms/rich-text/toolbar.js +1 -1
  249. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  250. package/dist/components/cms/rich-text/unknown-node.js +2 -2
  251. package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
  252. package/dist/components/cms/rich-text-preview.js +2 -2
  253. package/dist/components/cms/rich-text-preview.js.map +1 -1
  254. package/dist/components/cms/row-actions.d.ts +1 -1
  255. package/dist/components/cms/row-actions.js.map +1 -1
  256. package/dist/components/cms/scalar-value-editor.d.ts +1 -1
  257. package/dist/components/cms/scalar-value-editor.js +2 -2
  258. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  259. package/dist/components/cms/use-discard-controller.d.ts +1 -1
  260. package/dist/components/cms/use-discard-controller.js +1 -1
  261. package/dist/components/cms/use-discard-controller.js.map +1 -1
  262. package/dist/components/cms/use-document-draft.d.ts +7 -1
  263. package/dist/components/cms/use-document-draft.js +16 -2
  264. package/dist/components/cms/use-document-draft.js.map +1 -1
  265. package/dist/components/cms/use-document-versions.js +1 -1
  266. package/dist/components/cms/use-document-versions.js.map +1 -1
  267. package/dist/components/cms/use-field-focus.d.ts +1 -1
  268. package/dist/components/cms/use-field-focus.js +2 -2
  269. package/dist/components/cms/use-field-focus.js.map +1 -1
  270. package/dist/components/cms/use-live-url.d.ts +1 -1
  271. package/dist/components/cms/use-live-url.js +3 -3
  272. package/dist/components/cms/use-live-url.js.map +1 -1
  273. package/dist/components/cms/use-publish-controller.d.ts +1 -1
  274. package/dist/components/cms/use-publish-controller.js +1 -1
  275. package/dist/components/cms/use-publish-controller.js.map +1 -1
  276. package/dist/components/cms/use-published-perspective.d.ts +1 -1
  277. package/dist/components/cms/use-published-perspective.js.map +1 -1
  278. package/dist/components/cms/use-restore-version.d.ts +1 -1
  279. package/dist/components/cms/use-restore-version.js +1 -1
  280. package/dist/components/cms/use-restore-version.js.map +1 -1
  281. package/dist/components/cms/version-compare-lazy.js +1 -1
  282. package/dist/components/cms/version-compare-lazy.js.map +1 -1
  283. package/dist/components/cms/version-compare.js +1 -1
  284. package/dist/components/cms/version-compare.js.map +1 -1
  285. package/dist/components/cms/versions-pane.js +3 -3
  286. package/dist/components/cms/versions-pane.js.map +1 -1
  287. package/dist/components/cms/view-bar.js +3 -3
  288. package/dist/components/cms/view-bar.js.map +1 -1
  289. package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
  290. package/dist/components/collection-view/cms-collection-view-source.js +2 -2
  291. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  292. package/dist/components/collection-view/collection-view-context.d.ts +1 -1
  293. package/dist/components/collection-view/collection-view-context.js +1 -1
  294. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  295. package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
  296. package/dist/components/collection-view/collection-view-surface.js +18 -0
  297. package/dist/components/collection-view/collection-view-surface.js.map +1 -0
  298. package/dist/components/components/component-detail.js +10 -10
  299. package/dist/components/components/component-detail.js.map +1 -1
  300. package/dist/components/components/component-preview.d.ts +2 -2
  301. package/dist/components/components/component-preview.js +5 -5
  302. package/dist/components/components/component-preview.js.map +1 -1
  303. package/dist/components/components/component-status.d.ts +1 -1
  304. package/dist/components/components/component-status.js +1 -1
  305. package/dist/components/components/component-status.js.map +1 -1
  306. package/dist/components/components/components-index.js +2 -2
  307. package/dist/components/components/components-index.js.map +1 -1
  308. package/dist/components/components/field-inspector.js +1 -1
  309. package/dist/components/components/field-inspector.js.map +1 -1
  310. package/dist/components/components/registry-browser.js +3 -3
  311. package/dist/components/components/registry-browser.js.map +1 -1
  312. package/dist/components/components/taxonomy.d.ts +1 -1
  313. package/dist/components/components/taxonomy.js.map +1 -1
  314. package/dist/components/components/transfer-dialog.js +3 -3
  315. package/dist/components/components/transfer-dialog.js.map +1 -1
  316. package/dist/components/current-user-menu.js +1 -1
  317. package/dist/components/current-user-menu.js.map +1 -1
  318. package/dist/components/delivery-health-banner.js +2 -2
  319. package/dist/components/delivery-health-banner.js.map +1 -1
  320. package/dist/components/deployment-setup-gate.js +3 -3
  321. package/dist/components/deployment-setup-gate.js.map +1 -1
  322. package/dist/components/dev-login-badge.js +2 -2
  323. package/dist/components/dev-login-badge.js.map +1 -1
  324. package/dist/components/guards.js +1 -1
  325. package/dist/components/guards.js.map +1 -1
  326. package/dist/components/inspector/document-panes.d.ts +54 -0
  327. package/dist/components/inspector/document-panes.js +41 -0
  328. package/dist/components/inspector/document-panes.js.map +1 -0
  329. package/dist/components/inspector/inspector-sidebar.js +1 -1
  330. package/dist/components/inspector/inspector-sidebar.js.map +1 -1
  331. package/dist/components/nav-issues-registry.d.ts +53 -0
  332. package/dist/components/nav-issues-registry.js +69 -0
  333. package/dist/components/nav-issues-registry.js.map +1 -0
  334. package/dist/components/nav-issues.js +4 -4
  335. package/dist/components/nav-issues.js.map +1 -1
  336. package/dist/components/operation-error.d.ts +1 -1
  337. package/dist/components/operation-error.js +4 -4
  338. package/dist/components/operation-error.js.map +1 -1
  339. package/dist/components/playground-badge.js +2 -2
  340. package/dist/components/playground-badge.js.map +1 -1
  341. package/dist/components/project-list.js +3 -3
  342. package/dist/components/project-list.js.map +1 -1
  343. package/dist/components/project-session.d.ts +2 -2
  344. package/dist/components/project-session.js +5 -5
  345. package/dist/components/project-session.js.map +1 -1
  346. package/dist/components/project-switcher.js +2 -2
  347. package/dist/components/project-switcher.js.map +1 -1
  348. package/dist/components/rail-store-status.js +1 -1
  349. package/dist/components/rail-store-status.js.map +1 -1
  350. package/dist/components/rail-view-source.d.ts +6 -5
  351. package/dist/components/rail-view-source.js +4 -3
  352. package/dist/components/rail-view-source.js.map +1 -1
  353. package/dist/components/records/record-lens.d.ts +51 -0
  354. package/dist/components/records/record-lens.js +156 -0
  355. package/dist/components/records/record-lens.js.map +1 -0
  356. package/dist/components/records/record-rail.d.ts +86 -0
  357. package/dist/components/records/record-rail.js +241 -0
  358. package/dist/components/records/record-rail.js.map +1 -0
  359. package/dist/components/redirects/redirects-schema.js +1 -1
  360. package/dist/components/redirects/redirects-schema.js.map +1 -1
  361. package/dist/components/references/references-panel.js +2 -2
  362. package/dist/components/references/references-panel.js.map +1 -1
  363. package/dist/components/section-nav.js +27 -11
  364. package/dist/components/section-nav.js.map +1 -1
  365. package/dist/components/section-state.js +2 -2
  366. package/dist/components/section-state.js.map +1 -1
  367. package/dist/components/section-surface.js +2 -2
  368. package/dist/components/section-surface.js.map +1 -1
  369. package/dist/components/session-expired-overlay.js +3 -3
  370. package/dist/components/session-expired-overlay.js.map +1 -1
  371. package/dist/components/settings-form.js +1 -1
  372. package/dist/components/settings-form.js.map +1 -1
  373. package/dist/components/sign-in-needed-badge.js +1 -1
  374. package/dist/components/sign-in-needed-badge.js.map +1 -1
  375. package/dist/components/sign-in-view.d.ts +2 -2
  376. package/dist/components/sign-in-view.js +5 -5
  377. package/dist/components/sign-in-view.js.map +1 -1
  378. package/dist/components/studio-context.js +3 -3
  379. package/dist/components/studio-context.js.map +1 -1
  380. package/dist/components/studio-project-screen.js +30 -3
  381. package/dist/components/studio-project-screen.js.map +1 -1
  382. package/dist/components/studio-shell.js +6 -5
  383. package/dist/components/studio-shell.js.map +1 -1
  384. package/dist/components/theme-context.d.ts +1 -1
  385. package/dist/components/theme-context.js +2 -2
  386. package/dist/components/theme-context.js.map +1 -1
  387. package/dist/components/use-hydration-phase.d.ts +1 -1
  388. package/dist/components/use-hydration-phase.js +1 -1
  389. package/dist/components/use-hydration-phase.js.map +1 -1
  390. package/dist/components/users/invite-user-bar.d.ts +27 -0
  391. package/dist/components/users/invite-user-bar.js +25 -9
  392. package/dist/components/users/invite-user-bar.js.map +1 -1
  393. package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
  394. package/dist/components/users/role-policy-drift-issues.js +71 -0
  395. package/dist/components/users/role-policy-drift-issues.js.map +1 -0
  396. package/dist/components/users/users-collection-view-source.d.ts +1 -1
  397. package/dist/components/users/users-collection-view-source.js +4 -2
  398. package/dist/components/users/users-collection-view-source.js.map +1 -1
  399. package/dist/components/users/users-module.js +1 -1
  400. package/dist/components/users/users-module.js.map +1 -1
  401. package/dist/components/users/users-schema.d.ts +10 -1
  402. package/dist/components/users/users-schema.js +45 -25
  403. package/dist/components/users/users-schema.js.map +1 -1
  404. package/dist/components/users/users-store.js +3 -3
  405. package/dist/components/users/users-store.js.map +1 -1
  406. package/dist/components/workspace-projects.d.ts +1 -1
  407. package/dist/components/workspace-projects.js +1 -1
  408. package/dist/components/workspace-projects.js.map +1 -1
  409. package/dist/lib/ai/index.d.ts +1 -1
  410. package/dist/lib/ai/index.js +1 -1
  411. package/dist/lib/ai/index.js.map +1 -1
  412. package/dist/lib/ai/node-handler.d.ts +1 -1
  413. package/dist/lib/ai/node-handler.js.map +1 -1
  414. package/dist/lib/ai/translate-document.js +1 -1
  415. package/dist/lib/ai/translate-document.js.map +1 -1
  416. package/dist/lib/clipboard/paste-issues.js +1 -1
  417. package/dist/lib/clipboard/paste-issues.js.map +1 -1
  418. package/dist/lib/clipboard/paste-target.js +2 -2
  419. package/dist/lib/clipboard/paste-target.js.map +1 -1
  420. package/dist/lib/cms/published-documents.js +1 -1
  421. package/dist/lib/cms/published-documents.js.map +1 -1
  422. package/dist/lib/cms-views/view-model.d.ts +12 -12
  423. package/dist/lib/cms-views/view-model.js +2 -2
  424. package/dist/lib/cms-views/view-model.js.map +1 -1
  425. package/dist/lib/collection-view/collection-view-source.d.ts +7 -2
  426. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  427. package/dist/lib/data/content-session-refresh.d.ts +1 -1
  428. package/dist/lib/data/content-session-refresh.js.map +1 -1
  429. package/dist/lib/data/playground-unload-guard.d.ts +1 -1
  430. package/dist/lib/data/playground-unload-guard.js +1 -1
  431. package/dist/lib/data/playground-unload-guard.js.map +1 -1
  432. package/dist/lib/engine-version.d.ts +1 -1
  433. package/dist/lib/engine-version.js +1 -1
  434. package/dist/lib/engine-version.js.map +1 -1
  435. package/dist/lib/i18n/i18n.js +1 -1
  436. package/dist/lib/i18n/i18n.js.map +1 -1
  437. package/dist/lib/i18n/messages.de.js +4 -0
  438. package/dist/lib/i18n/messages.de.js.map +1 -1
  439. package/dist/lib/i18n/messages.en.d.ts +4 -0
  440. package/dist/lib/i18n/messages.en.js +8 -0
  441. package/dist/lib/i18n/messages.en.js.map +1 -1
  442. package/dist/lib/operations/project-runtime-config.d.ts +9 -0
  443. package/dist/lib/operations/project-runtime-config.js +13 -0
  444. package/dist/lib/operations/project-runtime-config.js.map +1 -1
  445. package/dist/lib/operations/studio-operations.d.ts +9 -1
  446. package/dist/lib/operations/studio-operations.js +6 -1
  447. package/dist/lib/operations/studio-operations.js.map +1 -1
  448. package/dist/lib/persistence/convex-adapter.d.ts +16 -3
  449. package/dist/lib/persistence/convex-adapter.js +78 -47
  450. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  451. package/dist/lib/persistence/index.js +14 -1
  452. package/dist/lib/persistence/index.js.map +1 -1
  453. package/dist/lib/persona/labels.js +1 -1
  454. package/dist/lib/persona/labels.js.map +1 -1
  455. package/dist/lib/query/operations-query.d.ts +1 -1
  456. package/dist/lib/query/operations-query.js +1 -1
  457. package/dist/lib/query/operations-query.js.map +1 -1
  458. package/dist/lib/references/reference-model.js +1 -1
  459. package/dist/lib/references/reference-model.js.map +1 -1
  460. package/dist/lib/references/references-store.js +1 -1
  461. package/dist/lib/references/references-store.js.map +1 -1
  462. package/dist/lib/workspace/index.d.ts +6 -0
  463. package/dist/lib/workspace/index.js +8 -0
  464. package/dist/lib/workspace/index.js.map +1 -1
  465. package/dist/mount.d.ts +28 -0
  466. package/dist/mount.js +27 -0
  467. package/dist/mount.js.map +1 -0
  468. package/dist/router/root.js +10 -10
  469. package/dist/router/root.js.map +1 -1
  470. package/dist/ui/badge.d.ts +1 -1
  471. package/dist/ui/button.d.ts +2 -2
  472. package/package.json +15 -11
  473. package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
  474. package/dist/components/redirects/redirects-collection-view-source.js +0 -146
  475. package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
  476. package/dist/components/redirects/redirects-module.d.ts +0 -21
  477. package/dist/components/redirects/redirects-module.js +0 -40
  478. package/dist/components/redirects/redirects-module.js.map +0 -1
  479. package/dist/components/redirects/redirects-store.d.ts +0 -68
  480. package/dist/components/redirects/redirects-store.js +0 -132
  481. package/dist/components/redirects/redirects-store.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"rail-store-status.js","sourceRoot":"","sources":["../../src/components/rail-store-status.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,UAAU,EACV,YAAY,EACZ,WAAW,GAaZ;IACC,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,6BAAkB,WAAW,YAC3B,KAAC,gBAAgB,IACf,KAAK,EAAC,OAAO,EACb,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,KAAK,CAAC,YAAY,IAAI,SAAS,CAAC,2BAA2B,CAAC,EACzE,MAAM,EACJ,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,cAAc,CAAC,IACjC,GAEX,GACE,CACP,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,KAAC,gBAAgB,IAAC,KAAK,EAAC,SAAS,EAAC,KAAK,EAAE,YAAY,GAAI,CAAA;IAClE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import type { ReactElement } from 'react'\nimport { RotateCcw } from 'lucide-react'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { SectionStateView } from './section-state'\n\n/**\n * The shared error/loading status gate for a rail module (EC-319/EC-323,\n * repointed onto `SectionStateView` in EC-327). The assets, users, and\n * redirects rails each read a list store with the same\n * `{ status, errorMessage, refresh }` shape; this renders the shared\n * error/loading section state — each rail's own translated `errorLabel`/\n * `loadingLabel` copy, not a generic one — instead of a rail re-inlining its\n * own AlertTriangle+retry panel. Returns `null` for a `ready` store.\n */\nexport function RailStoreStatus({\n store,\n errorLabel,\n loadingLabel,\n errorTestId,\n}: {\n store: {\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n refresh: () => Promise<void>\n }\n /** Localized \"…failed to load\" heading for this rail. */\n errorLabel: string\n /** Localized \"Loading …\" text for this rail. */\n loadingLabel: string\n /** data-testid on the error container (per-rail, for tests). */\n errorTestId: string\n}): ReactElement | null {\n if (store.status === 'error') {\n return (\n <div data-testid={errorTestId}>\n <SectionStateView\n state=\"error\"\n title={errorLabel}\n description={store.errorMessage ?? translate('common.backendUnreachable')}\n action={\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> {translate('common.retry')}\n </Button>\n }\n />\n </div>\n )\n }\n if (store.status === 'loading') {\n return <SectionStateView state=\"loading\" title={loadingLabel} />\n }\n return null\n}\n"]}
1
+ {"version":3,"file":"rail-store-status.js","sourceRoot":"","sources":["../../src/components/rail-store-status.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAElD;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,UAAU,EACV,YAAY,EACZ,WAAW,GAaZ;IACC,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,6BAAkB,WAAW,YAC3B,KAAC,gBAAgB,IACf,KAAK,EAAC,OAAO,EACb,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,KAAK,CAAC,YAAY,IAAI,SAAS,CAAC,2BAA2B,CAAC,EACzE,MAAM,EACJ,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,OAAE,SAAS,CAAC,cAAc,CAAC,IACjC,GAEX,GACE,CACP,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,KAAC,gBAAgB,IAAC,KAAK,EAAC,SAAS,EAAC,KAAK,EAAE,YAAY,GAAI,CAAA;IAClE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import type { ReactElement } from 'react'\nimport { RotateCcw } from 'lucide-react'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '../lib/i18n'\nimport { SectionStateView } from './section-state'\n\n/**\n * The shared error/loading status gate for a rail module (EC-319/EC-323,\n * repointed onto `SectionStateView` in EC-327). The assets, users, and\n * redirects rails each read a list store with the same\n * `{ status, errorMessage, refresh }` shape; this renders the shared\n * error/loading section state — each rail's own translated `errorLabel`/\n * `loadingLabel` copy, not a generic one — instead of a rail re-inlining its\n * own AlertTriangle+retry panel. Returns `null` for a `ready` store.\n */\nexport function RailStoreStatus({\n store,\n errorLabel,\n loadingLabel,\n errorTestId,\n}: {\n store: {\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n refresh: () => Promise<void>\n }\n /** Localized \"…failed to load\" heading for this rail. */\n errorLabel: string\n /** Localized \"Loading …\" text for this rail. */\n loadingLabel: string\n /** data-testid on the error container (per-rail, for tests). */\n errorTestId: string\n}): ReactElement | null {\n if (store.status === 'error') {\n return (\n <div data-testid={errorTestId}>\n <SectionStateView\n state=\"error\"\n title={errorLabel}\n description={store.errorMessage ?? translate('common.backendUnreachable')}\n action={\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> {translate('common.retry')}\n </Button>\n }\n />\n </div>\n )\n }\n if (store.status === 'loading') {\n return <SectionStateView state=\"loading\" title={loadingLabel} />\n }\n return null\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  import type { CmsDocument, CmsValidationIssue, CollectionDef, DocumentRef, FieldDef, LocaleConfig } from '@elytracms/core/cms-core';
2
2
  import type { OperationError, OperationResult } from '@elytracms/core/operations';
3
3
  import type { ReferenceTarget } from '@elytracms/core/persistence';
4
- import type { TableViewState } from '@elytracms/studio/lib/cms-views';
5
- import type { CollectionViewCapabilities, CollectionViewSource, FieldAccess } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ import type { TableViewState } from '../lib/cms-views/index.js';
5
+ import type { CollectionViewCapabilities, CollectionViewSource, FieldAccess } from '../lib/collection-view/collection-view-source.js';
6
6
  import type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions.js';
7
7
  /**
8
8
  * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks
@@ -48,7 +48,7 @@ export declare function metaBagProjection(values: Record<string, unknown>, canon
48
48
  /** Wrap a raw store write (save/remove) so a normalized failure surfaces via
49
49
  * `setError` — the factory's error-mapping plumbing, pulled out as a plain
50
50
  * function (no hook) so it is unit-testable without a `renderHook` harness. */
51
- export declare function createErrorMappedWriter<TArg, TResult>(write: (arg: TArg) => Promise<TResult>, normalize: (result: TResult) => OperationError | null, setError: (error: OperationError) => void): (arg: TArg) => void;
51
+ export declare function createErrorMappedWriter<TArg, TResult, TExtra = never>(write: (arg: TArg, extra?: TExtra) => Promise<TResult>, normalize: (result: TResult) => OperationError | null, setError: (error: OperationError) => void): (arg: TArg, extra?: TExtra) => void;
52
52
  /** The store-facing input of {@link runRailBulkAction} — plain data/functions,
53
53
  * so the rail bulk path is unit-testable without a hook harness. */
54
54
  export interface RailBulkInput {
@@ -110,7 +110,8 @@ interface RailViewSourceCore {
110
110
  * each rail's own store-result shape — reconciled to `lastError` by the
111
111
  * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */
112
112
  export interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {
113
- save(doc: CmsDocument): Promise<TSaveResult>;
113
+ /** `base`: the record as the editor opened it (EC-616 — see `RecordWrites.save`). */
114
+ save(doc: CmsDocument, base?: CmsDocument): Promise<TSaveResult>;
114
115
  remove(ref: DocumentRef): Promise<TRemoveResult>;
115
116
  /**
116
117
  * The delete the BULK path dispatches (EC-433) — always post-confirmation
@@ -130,7 +131,7 @@ interface RailViewSourceAssemblyInput extends RailViewSourceCore {
130
131
  clearLastError(): void;
131
132
  viewState: TableViewState;
132
133
  setViewState: CollectionViewSource['setViewState'];
133
- save(doc: CmsDocument): void;
134
+ save(doc: CmsDocument, base?: CmsDocument): void;
134
135
  remove(ref: DocumentRef): void;
135
136
  /**
136
137
  * The rail bulk runner (EC-433). Assembled onto the source ONLY when
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
- import { defaultViewState } from '@elytracms/studio/lib/cms-views';
2
+ import { defaultViewState } from '../lib/cms-views/index.js';
3
3
  import { runBulkAction } from './cms/bulk-actions.js';
4
4
  /** The default normalizer for an {@link OkResult}: a failure maps to a
5
5
  * surfaced {@link OperationError}; a success maps to no error at all. */
@@ -27,8 +27,9 @@ export function metaBagProjection(values, canonicalNames) {
27
27
  * `setError` — the factory's error-mapping plumbing, pulled out as a plain
28
28
  * function (no hook) so it is unit-testable without a `renderHook` harness. */
29
29
  export function createErrorMappedWriter(write, normalize, setError) {
30
- return (arg) => {
31
- void write(arg).then((result) => {
30
+ // `extra` (EC-616): a record save's base, forwarded untouched.
31
+ return (arg, extra) => {
32
+ void write(arg, extra).then((result) => {
32
33
  const error = normalize(result);
33
34
  if (error)
34
35
  setError(error);
@@ -1 +1 @@
1
- {"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/core/operations'\nimport type { ReferenceTarget } from '@elytracms/core/persistence'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { runBulkAction } from './cms/bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult>(\n write: (arg: TArg) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg) => void {\n return (arg: TArg) => {\n void write(arg).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The store-facing input of {@link runRailBulkAction} — plain data/functions,\n * so the rail bulk path is unit-testable without a hook harness. */\nexport interface RailBulkInput {\n collectionId: string\n /** Current rail rows (lens documents) — the runner's lookup + validation input. */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Dispatch ONE already-confirmed delete (the EC-173 reference dialog ran\n * before anything reaches this) and reconcile the rail store's result shape\n * into an operation-shaped outcome — a failure here is a visible per-item\n * failure in the report, never a swallowed one.\n */\n removeConfirmed(ref: DocumentRef): Promise<OperationResult<unknown>>\n}\n\n/**\n * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery\n * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),\n * wired to the rail's own store delete. Only `delete` is dispatchable — the\n * rails have no publish lifecycle and no bulk set-field — and an unsupported\n * action fails each item HONESTLY in the report rather than silently no-oping\n * (the exact bug this replaces: the assets rail's bulk delete confirmed into a\n * missing `source.bulk` and dropped the whole action on the floor).\n */\nexport function runRailBulkAction(\n input: RailBulkInput,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n return runBulkAction(\n {\n documents: input.documents,\n titleOf: input.titleOf,\n validate: input.validate,\n // A rail store patches its own cache/state inside a succeeded remove\n // (assets/users/redirects all do), and publish/unpublish/set-field never\n // succeed here (dispatch rejects them), so the session-apply hooks have\n // nothing left to apply.\n applyState: () => undefined,\n applyDelete: () => undefined,\n dispatch: (bulkAction, ref) =>\n bulkAction === 'delete'\n ? input.removeConfirmed(ref)\n : Promise.resolve<OperationResult<unknown>>({\n ok: false,\n error: {\n code: 'unknown',\n message: `Bulk ${bulkAction} is not supported in this view.`,\n },\n }),\n },\n input.collectionId,\n ids,\n action,\n setField,\n )\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n /**\n * How this rail's records are addressed in the reference index (EC-433) —\n * only the assets rail supplies one (`{ type: 'asset' }`); absent ⇒ the\n * confirm dialog's default document-shaped lookup.\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n save(doc: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n /**\n * The delete the BULK path dispatches (EC-433) — always post-confirmation\n * (the EC-173 reference dialog ran first). Defaults to {@link remove}; the\n * assets rail overrides it to pass `confirmInUse: true` so a confirmed bulk\n * delete of a referenced asset actually deletes (mirroring the detail\n * surface's confirmed delete) instead of re-blocking on the usage gate.\n */\n bulkRemove?(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * The rail bulk runner (EC-433). Assembled onto the source ONLY when\n * `capabilities.bulk` is set — the `CollectionViewSource` contract is\n * \"method present iff the flag is\" — so a bulk-less rail's list never\n * half-offers the affordance.\n */\n bulk?: CollectionViewSource['bulk']\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n ...(referenceTarget ? { referenceTarget } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n // EC-595 A2: a rail record is written whole, on the editor's explicit Save.\n writes: { kind: 'record', save },\n remove,\n // Present iff the capability flag is set (the CollectionViewSource contract).\n ...(capabilities.bulk && bulk ? { bulk } : {}),\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n bulkRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n // The rail bulk runner (EC-433): the shared `runBulkAction` machinery over\n // the rail's own confirmed delete, its store result reconciled into an\n // operation-shaped outcome (a failing remove is a visible per-item failure).\n // Assembled onto the source only when `capabilities.bulk` is set (below).\n const bulk = useCallback(\n (ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport> =>\n runRailBulkAction(\n {\n collectionId: schema.id,\n documents: documents(),\n titleOf,\n validate,\n removeConfirmed: async (ref) => {\n const result = await (bulkRemove ?? rawRemove)(ref)\n const error = normalizeRemoveResult(result)\n return error ? { ok: false, error } : { ok: true, value: undefined }\n },\n },\n ids,\n action,\n setField,\n ),\n [schema.id, documents, titleOf, validate, bulkRemove, rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n ],\n )\n}\n"]}
1
+ {"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/core/operations'\nimport type { ReferenceTarget } from '@elytracms/core/persistence'\nimport { defaultViewState } from '../lib/cms-views'\nimport type { TableViewState } from '../lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '../lib/collection-view/collection-view-source'\nimport { runBulkAction } from './cms/bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult, TExtra = never>(\n write: (arg: TArg, extra?: TExtra) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg, extra?: TExtra) => void {\n // `extra` (EC-616): a record save's base, forwarded untouched.\n return (arg: TArg, extra?: TExtra) => {\n void write(arg, extra).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The store-facing input of {@link runRailBulkAction} — plain data/functions,\n * so the rail bulk path is unit-testable without a hook harness. */\nexport interface RailBulkInput {\n collectionId: string\n /** Current rail rows (lens documents) — the runner's lookup + validation input. */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Dispatch ONE already-confirmed delete (the EC-173 reference dialog ran\n * before anything reaches this) and reconcile the rail store's result shape\n * into an operation-shaped outcome — a failure here is a visible per-item\n * failure in the report, never a swallowed one.\n */\n removeConfirmed(ref: DocumentRef): Promise<OperationResult<unknown>>\n}\n\n/**\n * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery\n * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),\n * wired to the rail's own store delete. Only `delete` is dispatchable — the\n * rails have no publish lifecycle and no bulk set-field — and an unsupported\n * action fails each item HONESTLY in the report rather than silently no-oping\n * (the exact bug this replaces: the assets rail's bulk delete confirmed into a\n * missing `source.bulk` and dropped the whole action on the floor).\n */\nexport function runRailBulkAction(\n input: RailBulkInput,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n return runBulkAction(\n {\n documents: input.documents,\n titleOf: input.titleOf,\n validate: input.validate,\n // A rail store patches its own cache/state inside a succeeded remove\n // (assets/users/redirects all do), and publish/unpublish/set-field never\n // succeed here (dispatch rejects them), so the session-apply hooks have\n // nothing left to apply.\n applyState: () => undefined,\n applyDelete: () => undefined,\n dispatch: (bulkAction, ref) =>\n bulkAction === 'delete'\n ? input.removeConfirmed(ref)\n : Promise.resolve<OperationResult<unknown>>({\n ok: false,\n error: {\n code: 'unknown',\n message: `Bulk ${bulkAction} is not supported in this view.`,\n },\n }),\n },\n input.collectionId,\n ids,\n action,\n setField,\n )\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n /**\n * How this rail's records are addressed in the reference index (EC-433) —\n * only the assets rail supplies one (`{ type: 'asset' }`); absent ⇒ the\n * confirm dialog's default document-shaped lookup.\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n /** `base`: the record as the editor opened it (EC-616 — see `RecordWrites.save`). */\n save(doc: CmsDocument, base?: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n /**\n * The delete the BULK path dispatches (EC-433) — always post-confirmation\n * (the EC-173 reference dialog ran first). Defaults to {@link remove}; the\n * assets rail overrides it to pass `confirmInUse: true` so a confirmed bulk\n * delete of a referenced asset actually deletes (mirroring the detail\n * surface's confirmed delete) instead of re-blocking on the usage gate.\n */\n bulkRemove?(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument, base?: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * The rail bulk runner (EC-433). Assembled onto the source ONLY when\n * `capabilities.bulk` is set — the `CollectionViewSource` contract is\n * \"method present iff the flag is\" — so a bulk-less rail's list never\n * half-offers the affordance.\n */\n bulk?: CollectionViewSource['bulk']\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n ...(referenceTarget ? { referenceTarget } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n // EC-595 A2: a rail record is written whole, on the editor's explicit Save.\n writes: { kind: 'record', save },\n remove,\n // Present iff the capability flag is set (the CollectionViewSource contract).\n ...(capabilities.bulk && bulk ? { bulk } : {}),\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n bulkRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n // The rail bulk runner (EC-433): the shared `runBulkAction` machinery over\n // the rail's own confirmed delete, its store result reconciled into an\n // operation-shaped outcome (a failing remove is a visible per-item failure).\n // Assembled onto the source only when `capabilities.bulk` is set (below).\n const bulk = useCallback(\n (ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport> =>\n runRailBulkAction(\n {\n collectionId: schema.id,\n documents: documents(),\n titleOf,\n validate,\n removeConfirmed: async (ref) => {\n const result = await (bulkRemove ?? rawRemove)(ref)\n const error = normalizeRemoveResult(result)\n return error ? { ok: false, error } : { ok: true, value: undefined }\n },\n },\n ids,\n action,\n setField,\n ),\n [schema.id, documents, titleOf, validate, bulkRemove, rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n ],\n )\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
2
+ import type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence';
3
+ /**
4
+ * EC-616 — the projections between a record kind and the portable list/edit surfaces
5
+ * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)
6
+ * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save
7
+ * writes is unit-testable on its own (it is the one place the write is derived from the
8
+ * edited lens; a slip here silently corrupts a record).
9
+ *
10
+ * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then
11
+ * the kind's columns — a canonical column always wins over a same-named meta key
12
+ * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a
13
+ * re-save wrote it into the canonical `to`).
14
+ * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is
15
+ * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2
16
+ * — a mount's additive field). A column the document does not carry is left out, so the
17
+ * stored value is kept (EC-577: a create default applies once, in the adapter's
18
+ * transaction — never restated here). A REQUIRED text column is always sent (`''` when
19
+ * blank): the editor owns it, and an empty one is refused visibly by the write's
20
+ * validation rather than kept silently. `meta` is omitted when no additive value exists,
21
+ * so a rail with no additive fields never touches the bag.
22
+ */
23
+ /** The kind's columns (its declared fields; `meta` is the bag, not a column). */
24
+ export declare function recordColumnNames(kind: AnyRecordKind): ReadonlySet<string>;
25
+ /** A stored record as the lens document the portable surfaces edit. */
26
+ export declare function recordToLensDocument<K extends AnyRecordKind>(kind: K, entry: RecordEntryOf<K>): CmsDocument;
27
+ /**
28
+ * An edited lens document as the record write (see the module header).
29
+ *
30
+ * **With `base`** — the lens document of the record as the editor OPENED it — the write
31
+ * carries only what the editor changed (EC-616, the multi-session baseline): a column whose
32
+ * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the
33
+ * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored
34
+ * value — a column another writer changed after the editor opened the record survives the
35
+ * editor's save. With nothing changed the write is just the id (a no-op edit).
36
+ *
37
+ * **Without `base`** (a record the editor is creating) the write is whole, as it always was.
38
+ */
39
+ export declare function lensDocumentToRecordWrite<K extends AnyRecordKind>(kind: K, doc: CmsDocument, base?: CmsDocument): RecordWriteOf<K>;
40
+ /**
41
+ * The DEFAULT lens of a kind: one field per column whose type the portable form edits
42
+ * (text, boolean, number, select — derived from the column's zod type), labelled from its
43
+ * name. A column of another shape (an object, a list) is not in the lens: the rail never
44
+ * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens
45
+ * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the
46
+ * built-in rails use), or passes its own `schema`.
47
+ */
48
+ export declare function recordLensDef(kind: AnyRecordKind, options?: {
49
+ label?: string;
50
+ titleField?: string;
51
+ }): CollectionDef;
@@ -0,0 +1,156 @@
1
+ import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core';
2
+ import { metaBagProjection } from '../rail-view-source.js';
3
+ /**
4
+ * EC-616 — the projections between a record kind and the portable list/edit surfaces
5
+ * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)
6
+ * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save
7
+ * writes is unit-testable on its own (it is the one place the write is derived from the
8
+ * edited lens; a slip here silently corrupts a record).
9
+ *
10
+ * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then
11
+ * the kind's columns — a canonical column always wins over a same-named meta key
12
+ * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a
13
+ * re-save wrote it into the canonical `to`).
14
+ * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is
15
+ * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2
16
+ * — a mount's additive field). A column the document does not carry is left out, so the
17
+ * stored value is kept (EC-577: a create default applies once, in the adapter's
18
+ * transaction — never restated here). A REQUIRED text column is always sent (`''` when
19
+ * blank): the editor owns it, and an empty one is refused visibly by the write's
20
+ * validation rather than kept silently. `meta` is omitted when no additive value exists,
21
+ * so a rail with no additive fields never touches the bag.
22
+ */
23
+ /** The kind's columns (its declared fields; `meta` is the bag, not a column). */
24
+ export function recordColumnNames(kind) {
25
+ return new Set(Object.keys(kind.fields));
26
+ }
27
+ /** A stored record as the lens document the portable surfaces edit. */
28
+ export function recordToLensDocument(kind, entry) {
29
+ const record = entry;
30
+ const columns = {};
31
+ for (const column of recordColumnNames(kind)) {
32
+ if (record[column] !== undefined)
33
+ columns[column] = record[column];
34
+ }
35
+ return {
36
+ collection: kind.kind,
37
+ id: String(record.id),
38
+ values: { ...(record.meta ?? {}), ...columns },
39
+ };
40
+ }
41
+ function defOf(field) {
42
+ return field._zod.def;
43
+ }
44
+ /** Is this column a required text column (no default, not optional)? */
45
+ function isRequiredText(field) {
46
+ return defOf(field).type === 'string';
47
+ }
48
+ /** The column's own definition, below any optional/default/nullable wrapper. */
49
+ function unwrapped(field) {
50
+ let def = defOf(field);
51
+ while ((def.type === 'optional' || def.type === 'default' || def.type === 'nullable') && def.innerType) {
52
+ def = def.innerType._zod.def;
53
+ }
54
+ return def;
55
+ }
56
+ /** A lens value as the write sends it for one column (a required text column is always a string). */
57
+ function columnWriteValue(field, value) {
58
+ return isRequiredText(field) ? (typeof value === 'string' ? value : '') : value;
59
+ }
60
+ /**
61
+ * An edited lens document as the record write (see the module header).
62
+ *
63
+ * **With `base`** — the lens document of the record as the editor OPENED it — the write
64
+ * carries only what the editor changed (EC-616, the multi-session baseline): a column whose
65
+ * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the
66
+ * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored
67
+ * value — a column another writer changed after the editor opened the record survives the
68
+ * editor's save. With nothing changed the write is just the id (a no-op edit).
69
+ *
70
+ * **Without `base`** (a record the editor is creating) the write is whole, as it always was.
71
+ */
72
+ export function lensDocumentToRecordWrite(kind, doc, base) {
73
+ const values = doc.values ?? {};
74
+ const write = { id: doc.id };
75
+ const columns = recordColumnNames(kind);
76
+ if (!base) {
77
+ for (const [column, field] of Object.entries(kind.fields)) {
78
+ const value = columnWriteValue(field, values[column]);
79
+ if (value !== undefined)
80
+ write[column] = value;
81
+ }
82
+ const meta = metaBagProjection(values, columns);
83
+ if (Object.keys(meta).length > 0)
84
+ write.meta = meta;
85
+ return write;
86
+ }
87
+ const before = base.values ?? {};
88
+ for (const [column, field] of Object.entries(kind.fields)) {
89
+ const value = values[column];
90
+ if (semanticValueEqual(value, before[column]))
91
+ continue;
92
+ // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.
93
+ write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value);
94
+ }
95
+ const meta = metaBagProjection(values, columns);
96
+ const baseMeta = metaBagProjection(before, columns);
97
+ const metaPatch = {};
98
+ for (const key of new Set([...Object.keys(meta), ...Object.keys(baseMeta)])) {
99
+ if (semanticValueEqual(meta[key], baseMeta[key]))
100
+ continue;
101
+ metaPatch[key] = meta[key] === undefined ? null : meta[key];
102
+ }
103
+ if (Object.keys(metaPatch).length > 0)
104
+ write.metaPatch = metaPatch;
105
+ return write;
106
+ }
107
+ /** The lens field type a column edits as (unwrapping optional/default); `undefined` = not lens-editable. */
108
+ function lensFieldType(field) {
109
+ switch (unwrapped(field).type) {
110
+ case 'string':
111
+ return 'text';
112
+ case 'boolean':
113
+ return 'boolean';
114
+ case 'number':
115
+ return 'number';
116
+ case 'enum':
117
+ return 'select';
118
+ default:
119
+ return undefined;
120
+ }
121
+ }
122
+ function humanize(name) {
123
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ');
124
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
125
+ }
126
+ /**
127
+ * The DEFAULT lens of a kind: one field per column whose type the portable form edits
128
+ * (text, boolean, number, select — derived from the column's zod type), labelled from its
129
+ * name. A column of another shape (an object, a list) is not in the lens: the rail never
130
+ * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens
131
+ * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the
132
+ * built-in rails use), or passes its own `schema`.
133
+ */
134
+ export function recordLensDef(kind, options = {}) {
135
+ const fields = [];
136
+ for (const [name, field] of Object.entries(kind.fields)) {
137
+ const type = lensFieldType(field);
138
+ if (!type)
139
+ continue;
140
+ const entry = { name, type, form: { label: humanize(name) } };
141
+ if (type === 'select') {
142
+ entry.options = Object.values(unwrapped(field).entries ?? {}).map((value) => ({ value: String(value), label: String(value) }));
143
+ }
144
+ fields.push(entry);
145
+ }
146
+ const titleField = options.titleField ?? fields.find((field) => field.type === 'text')?.name;
147
+ return collectionDefSchema.parse({
148
+ id: kind.kind,
149
+ // A record kind is not a CMS `kind`; 'document' just satisfies the lens schema.
150
+ kind: 'document',
151
+ ...(titleField ? { titleField } : {}),
152
+ form: { label: options.label ?? humanize(kind.module) },
153
+ fields,
154
+ });
155
+ }
156
+ //# sourceMappingURL=record-lens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-lens.js","sourceRoot":"","sources":["../../../src/components/records/record-lens.ts"],"names":[],"mappings":"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","sourcesContent":["import { collectionDefSchema, semanticValueEqual } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence'\nimport { metaBagProjection } from '../rail-view-source'\n\n/**\n * EC-616 — the projections between a record kind and the portable list/edit surfaces\n * (AD-19): a record rail edits a record as a LENS document (`{ collection, id, values }`)\n * over a `CollectionDef`. Pure — no React — so the mapping that decides what a save\n * writes is unit-testable on its own (it is the one place the write is derived from the\n * edited lens; a slip here silently corrupts a record).\n *\n * - **Read** ({@link recordToLensDocument}): the stored `meta` bag spread FIRST, then\n * the kind's columns — a canonical column always wins over a same-named meta key\n * (RESIDUE-3c: spread last, a stored `meta.to` displayed as the redirect's target and a\n * re-save wrote it into the canonical `to`).\n * - **Write** ({@link lensDocumentToRecordWrite}): every lens value keyed by a column is\n * that column; every other key round-trips through the open `meta` bag (EC-311 Tier 2\n * — a mount's additive field). A column the document does not carry is left out, so the\n * stored value is kept (EC-577: a create default applies once, in the adapter's\n * transaction — never restated here). A REQUIRED text column is always sent (`''` when\n * blank): the editor owns it, and an empty one is refused visibly by the write's\n * validation rather than kept silently. `meta` is omitted when no additive value exists,\n * so a rail with no additive fields never touches the bag.\n */\n\n/** The kind's columns (its declared fields; `meta` is the bag, not a column). */\nexport function recordColumnNames(kind: AnyRecordKind): ReadonlySet<string> {\n return new Set(Object.keys(kind.fields))\n}\n\n/** A stored record as the lens document the portable surfaces edit. */\nexport function recordToLensDocument<K extends AnyRecordKind>(kind: K, entry: RecordEntryOf<K>): CmsDocument {\n const record = entry as Record<string, unknown>\n const columns: Record<string, unknown> = {}\n for (const column of recordColumnNames(kind)) {\n if (record[column] !== undefined) columns[column] = record[column]\n }\n return {\n collection: kind.kind,\n id: String(record.id),\n values: { ...((record.meta as Record<string, unknown> | undefined) ?? {}), ...columns },\n }\n}\n\n/**\n * A column's zod definition, read through `_zod.def` rather than `instanceof`: a kind is\n * declared in whichever package owns it (an addon), whose `zod` may be a different copy\n * than this one — `instanceof` across copies is silently false.\n */\ninterface ZodDefView {\n type: string\n innerType?: { _zod: { def: ZodDefView } }\n entries?: Record<string, unknown>\n}\nfunction defOf(field: unknown): ZodDefView {\n return (field as { _zod: { def: ZodDefView } })._zod.def\n}\n\n/** Is this column a required text column (no default, not optional)? */\nfunction isRequiredText(field: unknown): boolean {\n return defOf(field).type === 'string'\n}\n\n/** The column's own definition, below any optional/default/nullable wrapper. */\nfunction unwrapped(field: unknown): ZodDefView {\n let def = defOf(field)\n while ((def.type === 'optional' || def.type === 'default' || def.type === 'nullable') && def.innerType) {\n def = def.innerType._zod.def\n }\n return def\n}\n\n/** A lens value as the write sends it for one column (a required text column is always a string). */\nfunction columnWriteValue(field: unknown, value: unknown): unknown {\n return isRequiredText(field) ? (typeof value === 'string' ? value : '') : value\n}\n\n/**\n * An edited lens document as the record write (see the module header).\n *\n * **With `base`** — the lens document of the record as the editor OPENED it — the write\n * carries only what the editor changed (EC-616, the multi-session baseline): a column whose\n * value differs from the base, and the changed `meta` keys as a per-key `metaPatch` (a key the\n * editor removed is `null`). Every other column is ABSENT, so the EC-577 rule keeps the stored\n * value — a column another writer changed after the editor opened the record survives the\n * editor's save. With nothing changed the write is just the id (a no-op edit).\n *\n * **Without `base`** (a record the editor is creating) the write is whole, as it always was.\n */\nexport function lensDocumentToRecordWrite<K extends AnyRecordKind>(\n kind: K,\n doc: CmsDocument,\n base?: CmsDocument,\n): RecordWriteOf<K> {\n const values = doc.values ?? {}\n const write: Record<string, unknown> = { id: doc.id }\n const columns = recordColumnNames(kind)\n if (!base) {\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = columnWriteValue(field, values[column])\n if (value !== undefined) write[column] = value\n }\n const meta = metaBagProjection(values, columns)\n if (Object.keys(meta).length > 0) write.meta = meta\n return write as RecordWriteOf<K>\n }\n const before = base.values ?? {}\n for (const [column, field] of Object.entries(kind.fields)) {\n const value = values[column]\n if (semanticValueEqual(value, before[column])) continue\n // A cleared optional column is `null` on the wire (EC-577: clear), never a silent absence.\n write[column] = value === undefined ? (isRequiredText(field) ? '' : null) : columnWriteValue(field, value)\n }\n const meta = metaBagProjection(values, columns)\n const baseMeta = metaBagProjection(before, columns)\n const metaPatch: Record<string, unknown> = {}\n for (const key of new Set([...Object.keys(meta), ...Object.keys(baseMeta)])) {\n if (semanticValueEqual(meta[key], baseMeta[key])) continue\n metaPatch[key] = meta[key] === undefined ? null : meta[key]\n }\n if (Object.keys(metaPatch).length > 0) write.metaPatch = metaPatch\n return write as RecordWriteOf<K>\n}\n\n/** The lens field type a column edits as (unwrapping optional/default); `undefined` = not lens-editable. */\nfunction lensFieldType(field: unknown): FieldDef['type'] | undefined {\n switch (unwrapped(field).type) {\n case 'string':\n return 'text'\n case 'boolean':\n return 'boolean'\n case 'number':\n return 'number'\n case 'enum':\n return 'select'\n default:\n return undefined\n }\n}\n\nfunction humanize(name: string): string {\n const spaced = name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')\n return spaced.charAt(0).toUpperCase() + spaced.slice(1)\n}\n\n/**\n * The DEFAULT lens of a kind: one field per column whose type the portable form edits\n * (text, boolean, number, select — derived from the column's zod type), labelled from its\n * name. A column of another shape (an object, a list) is not in the lens: the rail never\n * writes it, so the stored value is kept on every save (EC-577). A mount refines the lens\n * by name through `nav.rail(kind, { fields })` (EC-311 — the same override mechanism the\n * built-in rails use), or passes its own `schema`.\n */\nexport function recordLensDef(kind: AnyRecordKind, options: { label?: string; titleField?: string } = {}): CollectionDef {\n const fields: Record<string, unknown>[] = []\n for (const [name, field] of Object.entries(kind.fields)) {\n const type = lensFieldType(field)\n if (!type) continue\n const entry: Record<string, unknown> = { name, type, form: { label: humanize(name) } }\n if (type === 'select') {\n entry.options = Object.values(unwrapped(field).entries ?? {}).map((value) => ({ value: String(value), label: String(value) }))\n }\n fields.push(entry)\n }\n const titleField = options.titleField ?? fields.find((field) => field.type === 'text')?.name\n return collectionDefSchema.parse({\n id: kind.kind,\n // A record kind is not a CMS `kind`; 'document' just satisfies the lens schema.\n kind: 'document',\n ...(titleField ? { titleField } : {}),\n form: { label: options.label ?? humanize(kind.module) },\n fields,\n })\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { CollectionDef } from '@elytracms/core/cms-core';
3
+ import type { AnyRecordKind, RecordEntryOf, RecordWriteOf } from '@elytracms/core/persistence';
4
+ import type { CollectionViewCapabilities, CollectionViewSource } from '../../lib/collection-view/collection-view-source.js';
5
+ /** The default capability profile of a record rail: create, edit, remove, bulk delete — content, not administration. */
6
+ export declare const RECORD_RAIL_CAPABILITIES: CollectionViewCapabilities;
7
+ /** The one query key of a kind's records in a project. */
8
+ export declare function recordListKey(kind: AnyRecordKind, projectId: string): readonly [string, string, string];
9
+ /**
10
+ * A kind's records in a project, read through the shared operations cache — the ONE query
11
+ * a rail and any reader outside it (the rail foot's URL-map validation reads redirects)
12
+ * share. One key, one cache entry: a save's `cache.patch` updates both at once.
13
+ */
14
+ export declare function useRecordList<K extends AnyRecordKind>(kind: K, projectId: string): import("@tanstack/react-query").UseQueryResult<RecordEntryOf<K>[]>;
15
+ export interface RecordRailStore<K extends AnyRecordKind = AnyRecordKind> {
16
+ kind: K;
17
+ projectId: string;
18
+ status: 'loading' | 'ready' | 'error';
19
+ errorMessage?: string;
20
+ /** The persisted records, in the kind's order. Unsaved drafts are excluded. */
21
+ records: readonly RecordEntryOf<K>[];
22
+ /** One record: a local unsaved draft if present, else a persisted record. */
23
+ get(id: string): RecordEntryOf<K> | undefined;
24
+ /** Mint a blank in-memory draft and return its id (the "New" navigation target). */
25
+ createDraft(): string;
26
+ /** Persist a create/modify; drops the matching draft on success. */
27
+ save(input: RecordWriteOf<K>): Promise<{
28
+ ok: true;
29
+ record: RecordEntryOf<K>;
30
+ } | {
31
+ ok: false;
32
+ message: string;
33
+ }>;
34
+ /** Delete a persisted record, or silently discard an unsaved draft. */
35
+ remove(id: string): Promise<{
36
+ ok: true;
37
+ } | {
38
+ ok: false;
39
+ message: string;
40
+ }>;
41
+ /** Re-list from the backend. */
42
+ refresh(): Promise<void>;
43
+ }
44
+ /** The record rail store of the nearest {@link RecordRailProvider}. */
45
+ export declare function useRecordRail<K extends AnyRecordKind>(): RecordRailStore<K>;
46
+ export declare function RecordRailProvider<K extends AnyRecordKind>({ kind, projectId, children, }: {
47
+ kind: K;
48
+ projectId: string;
49
+ children: ReactNode;
50
+ }): import("react").JSX.Element;
51
+ /**
52
+ * The columns a blank "New" draft shows: each column's CREATE default (a new redirect
53
+ * reads enabled + permanent — the reason you create one is for it to work), and `''` for
54
+ * a required text column. The save then sends what the editor saw; the adapter still
55
+ * applies the defaults itself for a write that leaves them out (EC-577).
56
+ */
57
+ export declare function recordDraftColumns(kind: AnyRecordKind): Record<string, unknown>;
58
+ /** How a rail addresses its list and one record's detail (the view's own route segment). */
59
+ export interface RecordRailPaths {
60
+ listPath: string;
61
+ detailPath(id: string): string;
62
+ }
63
+ /**
64
+ * The {@link CollectionViewSource} of a record rail (must be mounted under its
65
+ * {@link RecordRailProvider}). `schema` is the rail's lens — the kind's default lens or a
66
+ * mount-customized one (EC-311 overrides by name; additive fields persist in `meta`).
67
+ */
68
+ export declare function useRecordCollectionViewSource<K extends AnyRecordKind>(kind: K, projectId: string, schema: CollectionDef, paths: RecordRailPaths, capabilities?: CollectionViewCapabilities): CollectionViewSource;
69
+ export interface RecordRailModuleProps<K extends AnyRecordKind> {
70
+ kind: K;
71
+ projectId: string;
72
+ /** `null` → the list; an id → that record's detail. */
73
+ recordId: string | null;
74
+ schema: CollectionDef;
75
+ paths: RecordRailPaths;
76
+ capabilities?: CollectionViewCapabilities;
77
+ /** The rail's label, for its loading / failure states. */
78
+ label: string;
79
+ /** Optional copy for the loading / failure states (default: the generic catalog keys). */
80
+ messages?: {
81
+ loading?: () => string;
82
+ loadFailed?: () => string;
83
+ };
84
+ }
85
+ /** The surface of a record rail: the portable list, or one record's detail. */
86
+ export declare function RecordRailModule<K extends AnyRecordKind>(props: RecordRailModuleProps<K>): import("react").JSX.Element;