@elytracms/studio 0.0.91 → 0.0.93

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 (353) hide show
  1. package/convex/asset-alt-migration.test.ts +127 -0
  2. package/convex/asset-replace.test.ts +168 -0
  3. package/convex/asset-revalidate.test.ts +6 -6
  4. package/convex/assets.ts +41 -5
  5. package/convex/cms.ts +537 -177
  6. package/convex/contract-parity.test.ts +173 -19
  7. package/convex/delivery.ts +26 -13
  8. package/convex/document-revalidate.test.ts +17 -16
  9. package/convex/orphan-hydration.test.ts +116 -0
  10. package/convex/presence-cost.test.ts +89 -0
  11. package/convex/rail-write-parity.test.ts +17 -14
  12. package/convex/referenceMaintenance.ts +15 -15
  13. package/convex/schema.ts +70 -5
  14. package/convex/triggers.ts +21 -8
  15. package/convex/write-limits.ts +1 -1
  16. package/dist/components/ai/ai-chat-panel.js +33 -22
  17. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  18. package/dist/components/ai/proposal-card.js +2 -1
  19. package/dist/components/ai/proposal-card.js.map +1 -1
  20. package/dist/components/ai/translate-document-action.d.ts +8 -2
  21. package/dist/components/ai/translate-document-action.js.map +1 -1
  22. package/dist/components/ai/translate-document-dialog.d.ts +1 -1
  23. package/dist/components/ai/translate-document-dialog.js +14 -8
  24. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  25. package/dist/components/ai/translate-skipped.d.ts +19 -0
  26. package/dist/components/ai/translate-skipped.js +46 -0
  27. package/dist/components/ai/translate-skipped.js.map +1 -0
  28. package/dist/components/app-views/default-modules.d.ts +23 -6
  29. package/dist/components/app-views/default-modules.js +33 -14
  30. package/dist/components/app-views/default-modules.js.map +1 -1
  31. package/dist/components/assets/asset-detail.js +18 -8
  32. package/dist/components/assets/asset-detail.js.map +1 -1
  33. package/dist/components/assets/asset-metadata.d.ts +15 -4
  34. package/dist/components/assets/asset-metadata.js +15 -2
  35. package/dist/components/assets/asset-metadata.js.map +1 -1
  36. package/dist/components/assets/asset-search.js +4 -1
  37. package/dist/components/assets/asset-search.js.map +1 -1
  38. package/dist/components/assets/asset-selection.d.ts +0 -8
  39. package/dist/components/assets/asset-selection.js +4 -23
  40. package/dist/components/assets/asset-selection.js.map +1 -1
  41. package/dist/components/assets/asset-thumb.js +3 -2
  42. package/dist/components/assets/asset-thumb.js.map +1 -1
  43. package/dist/components/assets/assets-collection-view-source.d.ts +3 -3
  44. package/dist/components/assets/assets-collection-view-source.js +9 -5
  45. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  46. package/dist/components/assets/assets-store.d.ts +14 -0
  47. package/dist/components/assets/assets-store.js +23 -1
  48. package/dist/components/assets/assets-store.js.map +1 -1
  49. package/dist/components/assets/replace-file.d.ts +22 -0
  50. package/dist/components/assets/replace-file.js +59 -0
  51. package/dist/components/assets/replace-file.js.map +1 -0
  52. package/dist/components/builder/blocks-model.d.ts +14 -26
  53. package/dist/components/builder/blocks-model.js +35 -41
  54. package/dist/components/builder/blocks-model.js.map +1 -1
  55. package/dist/components/builder/blocks-panel.d.ts +7 -3
  56. package/dist/components/builder/blocks-panel.js +6 -5
  57. package/dist/components/builder/blocks-panel.js.map +1 -1
  58. package/dist/components/builder/builder-store.d.ts +126 -30
  59. package/dist/components/builder/builder-store.js +169 -67
  60. package/dist/components/builder/builder-store.js.map +1 -1
  61. package/dist/components/builder/canvas-body.d.ts +30 -0
  62. package/dist/components/builder/canvas-body.js +38 -0
  63. package/dist/components/builder/canvas-body.js.map +1 -0
  64. package/dist/components/builder/canvas-chrome-css.js +7 -0
  65. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  66. package/dist/components/builder/canvas-dnd.d.ts +7 -7
  67. package/dist/components/builder/canvas-dnd.js +5 -5
  68. package/dist/components/builder/canvas-dnd.js.map +1 -1
  69. package/dist/components/builder/canvas-render-context.d.ts +30 -22
  70. package/dist/components/builder/canvas-render-context.js +44 -44
  71. package/dist/components/builder/canvas-render-context.js.map +1 -1
  72. package/dist/components/builder/canvas-runs.d.ts +45 -0
  73. package/dist/components/builder/canvas-runs.js +51 -0
  74. package/dist/components/builder/canvas-runs.js.map +1 -0
  75. package/dist/components/builder/canvas.d.ts +5 -4
  76. package/dist/components/builder/canvas.js +34 -6
  77. package/dist/components/builder/canvas.js.map +1 -1
  78. package/dist/components/builder/clipboard-actions.d.ts +15 -0
  79. package/dist/components/builder/clipboard-actions.js +50 -13
  80. package/dist/components/builder/clipboard-actions.js.map +1 -1
  81. package/dist/components/builder/composition-field-editor.d.ts +10 -16
  82. package/dist/components/builder/composition-field-editor.js +30 -40
  83. package/dist/components/builder/composition-field-editor.js.map +1 -1
  84. package/dist/components/builder/composition-patches.d.ts +7 -0
  85. package/dist/components/builder/composition-patches.js +256 -0
  86. package/dist/components/builder/composition-patches.js.map +1 -0
  87. package/dist/components/builder/delete-key.d.ts +5 -0
  88. package/dist/components/builder/delete-key.js +4 -0
  89. package/dist/components/builder/delete-key.js.map +1 -1
  90. package/dist/components/builder/insert-context.d.ts +17 -25
  91. package/dist/components/builder/insert-context.js +45 -92
  92. package/dist/components/builder/insert-context.js.map +1 -1
  93. package/dist/components/builder/page-tree.d.ts +3 -3
  94. package/dist/components/builder/page-tree.js +3 -3
  95. package/dist/components/builder/page-tree.js.map +1 -1
  96. package/dist/components/builder/placement-refusal.d.ts +18 -0
  97. package/dist/components/builder/placement-refusal.js +46 -0
  98. package/dist/components/builder/placement-refusal.js.map +1 -0
  99. package/dist/components/builder/property-inspector.js +4 -0
  100. package/dist/components/builder/property-inspector.js.map +1 -1
  101. package/dist/components/builder/reuse-context.js +65 -26
  102. package/dist/components/builder/reuse-context.js.map +1 -1
  103. package/dist/components/builder/reuse.d.ts +79 -8
  104. package/dist/components/builder/reuse.js +71 -23
  105. package/dist/components/builder/reuse.js.map +1 -1
  106. package/dist/components/builder/run-inspector.d.ts +11 -0
  107. package/dist/components/builder/run-inspector.js +33 -0
  108. package/dist/components/builder/run-inspector.js.map +1 -0
  109. package/dist/components/cms/asset-field.d.ts +38 -1
  110. package/dist/components/cms/asset-field.js +182 -58
  111. package/dist/components/cms/asset-field.js.map +1 -1
  112. package/dist/components/cms/asset-picker-core.d.ts +16 -0
  113. package/dist/components/cms/asset-picker-core.js +20 -0
  114. package/dist/components/cms/asset-picker-core.js.map +1 -1
  115. package/dist/components/cms/asset-picker.js +12 -18
  116. package/dist/components/cms/asset-picker.js.map +1 -1
  117. package/dist/components/cms/bulk-actions.d.ts +4 -20
  118. package/dist/components/cms/bulk-actions.js +3 -24
  119. package/dist/components/cms/bulk-actions.js.map +1 -1
  120. package/dist/components/cms/cell-edit.d.ts +5 -5
  121. package/dist/components/cms/cell-edit.js.map +1 -1
  122. package/dist/components/cms/cell.js.map +1 -1
  123. package/dist/components/cms/cms-store.d.ts +87 -45
  124. package/dist/components/cms/cms-store.js +347 -203
  125. package/dist/components/cms/cms-store.js.map +1 -1
  126. package/dist/components/cms/collection-table.js +7 -2
  127. package/dist/components/cms/collection-table.js.map +1 -1
  128. package/dist/components/cms/commit-policy.d.ts +24 -0
  129. package/dist/components/cms/commit-policy.js +37 -0
  130. package/dist/components/cms/commit-policy.js.map +1 -0
  131. package/dist/components/cms/detail-editor.js +83 -137
  132. package/dist/components/cms/detail-editor.js.map +1 -1
  133. package/dist/components/cms/document-diff-view.d.ts +3 -1
  134. package/dist/components/cms/document-diff-view.js +12 -7
  135. package/dist/components/cms/document-diff-view.js.map +1 -1
  136. package/dist/components/cms/draft-commit.d.ts +70 -0
  137. package/dist/components/cms/draft-commit.js +82 -0
  138. package/dist/components/cms/draft-commit.js.map +1 -0
  139. package/dist/components/cms/duplicate-document.d.ts +11 -13
  140. package/dist/components/cms/duplicate-document.js +26 -34
  141. package/dist/components/cms/duplicate-document.js.map +1 -1
  142. package/dist/components/cms/field-anchors.d.ts +7 -4
  143. package/dist/components/cms/field-anchors.js +9 -7
  144. package/dist/components/cms/field-anchors.js.map +1 -1
  145. package/dist/components/cms/field-collaboration.d.ts +47 -0
  146. package/dist/components/cms/field-collaboration.js +87 -0
  147. package/dist/components/cms/field-collaboration.js.map +1 -0
  148. package/dist/components/cms/field-control-lib.d.ts +22 -1
  149. package/dist/components/cms/field-control-lib.js +33 -4
  150. package/dist/components/cms/field-control-lib.js.map +1 -1
  151. package/dist/components/cms/field-control.js +17 -14
  152. package/dist/components/cms/field-control.js.map +1 -1
  153. package/dist/components/cms/gate-validation.d.ts +21 -30
  154. package/dist/components/cms/gate-validation.js +11 -22
  155. package/dist/components/cms/gate-validation.js.map +1 -1
  156. package/dist/components/cms/object-array-field.js +4 -4
  157. package/dist/components/cms/object-array-field.js.map +1 -1
  158. package/dist/components/cms/prop-fields.d.ts +7 -2
  159. package/dist/components/cms/prop-fields.js +13 -6
  160. package/dist/components/cms/prop-fields.js.map +1 -1
  161. package/dist/components/cms/reference-confirm.js +10 -4
  162. package/dist/components/cms/reference-confirm.js.map +1 -1
  163. package/dist/components/cms/rich-text/embed-picker.d.ts +1 -1
  164. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  165. package/dist/components/cms/rich-text/insert.d.ts +20 -9
  166. package/dist/components/cms/rich-text/insert.js +25 -17
  167. package/dist/components/cms/rich-text/insert.js.map +1 -1
  168. package/dist/components/cms/use-document-draft.d.ts +96 -0
  169. package/dist/components/cms/use-document-draft.js +283 -0
  170. package/dist/components/cms/use-document-draft.js.map +1 -0
  171. package/dist/components/cms/use-document-versions.js +2 -2
  172. package/dist/components/cms/use-document-versions.js.map +1 -1
  173. package/dist/components/cms/use-live-url.d.ts +6 -12
  174. package/dist/components/cms/use-live-url.js +16 -15
  175. package/dist/components/cms/use-live-url.js.map +1 -1
  176. package/dist/components/cms/use-publish-controller.d.ts +13 -18
  177. package/dist/components/cms/use-publish-controller.js +24 -41
  178. package/dist/components/cms/use-publish-controller.js.map +1 -1
  179. package/dist/components/cms/use-published-perspective.d.ts +2 -3
  180. package/dist/components/cms/use-published-perspective.js +4 -3
  181. package/dist/components/cms/use-published-perspective.js.map +1 -1
  182. package/dist/components/cms/versions-pane.js +8 -1
  183. package/dist/components/cms/versions-pane.js.map +1 -1
  184. package/dist/components/collection-view/cms-collection-view-source.js +24 -7
  185. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  186. package/dist/components/components/transfer-dialog.js +28 -23
  187. package/dist/components/components/transfer-dialog.js.map +1 -1
  188. package/dist/components/components/transfer.d.ts +31 -21
  189. package/dist/components/components/transfer.js +49 -35
  190. package/dist/components/components/transfer.js.map +1 -1
  191. package/dist/components/project-list.js.map +1 -1
  192. package/dist/components/project-session.d.ts +6 -3
  193. package/dist/components/project-session.js +14 -8
  194. package/dist/components/project-session.js.map +1 -1
  195. package/dist/components/project-switcher.js.map +1 -1
  196. package/dist/components/rail-view-source.js +2 -1
  197. package/dist/components/rail-view-source.js.map +1 -1
  198. package/dist/components/section-nav.js +1 -1
  199. package/dist/components/section-nav.js.map +1 -1
  200. package/dist/lib/ai/apply-content.js +56 -116
  201. package/dist/lib/ai/apply-content.js.map +1 -1
  202. package/dist/lib/ai/chat-context.d.ts +8 -0
  203. package/dist/lib/ai/chat-context.js +37 -6
  204. package/dist/lib/ai/chat-context.js.map +1 -1
  205. package/dist/lib/ai/chat-state.d.ts +14 -1
  206. package/dist/lib/ai/chat-state.js.map +1 -1
  207. package/dist/lib/ai/constraints.d.ts +20 -5
  208. package/dist/lib/ai/constraints.js +125 -33
  209. package/dist/lib/ai/constraints.js.map +1 -1
  210. package/dist/lib/ai/fake-provider.d.ts +16 -3
  211. package/dist/lib/ai/fake-provider.js +22 -6
  212. package/dist/lib/ai/fake-provider.js.map +1 -1
  213. package/dist/lib/ai/index.d.ts +2 -0
  214. package/dist/lib/ai/index.js +2 -0
  215. package/dist/lib/ai/index.js.map +1 -1
  216. package/dist/lib/ai/leash.d.ts +34 -0
  217. package/dist/lib/ai/leash.js +28 -0
  218. package/dist/lib/ai/leash.js.map +1 -0
  219. package/dist/lib/ai/provenance.d.ts +1 -1
  220. package/dist/lib/ai/provenance.js.map +1 -1
  221. package/dist/lib/ai/rich-text-input.d.ts +8 -8
  222. package/dist/lib/ai/rich-text-input.js +2 -1
  223. package/dist/lib/ai/rich-text-input.js.map +1 -1
  224. package/dist/lib/ai/rich-text-translate.d.ts +27 -4
  225. package/dist/lib/ai/rich-text-translate.js +28 -10
  226. package/dist/lib/ai/rich-text-translate.js.map +1 -1
  227. package/dist/lib/ai/shape.d.ts +13 -13
  228. package/dist/lib/ai/shape.js +69 -93
  229. package/dist/lib/ai/shape.js.map +1 -1
  230. package/dist/lib/ai/tools.d.ts +36 -41
  231. package/dist/lib/ai/tools.js +85 -87
  232. package/dist/lib/ai/tools.js.map +1 -1
  233. package/dist/lib/ai/translate-document.d.ts +39 -5
  234. package/dist/lib/ai/translate-document.js +160 -51
  235. package/dist/lib/ai/translate-document.js.map +1 -1
  236. package/dist/lib/ai/types.d.ts +34 -23
  237. package/dist/lib/ai/types.js.map +1 -1
  238. package/dist/lib/ai/update-fields.d.ts +21 -12
  239. package/dist/lib/ai/update-fields.js +118 -177
  240. package/dist/lib/ai/update-fields.js.map +1 -1
  241. package/dist/lib/ai/writer-form.d.ts +116 -0
  242. package/dist/lib/ai/writer-form.js +429 -0
  243. package/dist/lib/ai/writer-form.js.map +1 -0
  244. package/dist/lib/ai-current-document.d.ts +11 -11
  245. package/dist/lib/ai-current-document.js +4 -4
  246. package/dist/lib/ai-current-document.js.map +1 -1
  247. package/dist/lib/assets/upload.d.ts +31 -2
  248. package/dist/lib/assets/upload.js +61 -6
  249. package/dist/lib/assets/upload.js.map +1 -1
  250. package/dist/lib/clipboard/os-clipboard.d.ts +10 -2
  251. package/dist/lib/clipboard/os-clipboard.js +8 -4
  252. package/dist/lib/clipboard/os-clipboard.js.map +1 -1
  253. package/dist/lib/clipboard/paste-target.d.ts +18 -9
  254. package/dist/lib/clipboard/paste-target.js +8 -7
  255. package/dist/lib/clipboard/paste-target.js.map +1 -1
  256. package/dist/lib/cms/field-patch.d.ts +15 -0
  257. package/dist/lib/cms/field-patch.js +25 -0
  258. package/dist/lib/cms/field-patch.js.map +1 -0
  259. package/dist/lib/cms/published-documents.d.ts +1 -1
  260. package/dist/lib/cms/published-documents.js +1 -1
  261. package/dist/lib/cms/published-documents.js.map +1 -1
  262. package/dist/lib/cms-views/view-model.d.ts +3 -3
  263. package/dist/lib/collection-view/__fixtures__/document-writes.d.ts +6 -0
  264. package/dist/lib/collection-view/__fixtures__/document-writes.js +23 -0
  265. package/dist/lib/collection-view/__fixtures__/document-writes.js.map +1 -0
  266. package/dist/lib/collection-view/collection-view-source.d.ts +69 -6
  267. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  268. package/dist/lib/data/collection-content.d.ts +4 -13
  269. package/dist/lib/data/collection-content.js +11 -5
  270. package/dist/lib/data/collection-content.js.map +1 -1
  271. package/dist/lib/dnd/drop-rules.d.ts +25 -31
  272. package/dist/lib/dnd/drop-rules.js +53 -59
  273. package/dist/lib/dnd/drop-rules.js.map +1 -1
  274. package/dist/lib/engine-version.d.ts +1 -1
  275. package/dist/lib/engine-version.js +1 -1
  276. package/dist/lib/engine-version.js.map +1 -1
  277. package/dist/lib/i18n/messages.de.js +72 -15
  278. package/dist/lib/i18n/messages.de.js.map +1 -1
  279. package/dist/lib/i18n/messages.en.d.ts +61 -12
  280. package/dist/lib/i18n/messages.en.js +76 -16
  281. package/dist/lib/i18n/messages.en.js.map +1 -1
  282. package/dist/lib/operations/__fixtures__/write-document.d.ts +30 -0
  283. package/dist/lib/operations/__fixtures__/write-document.js +16 -0
  284. package/dist/lib/operations/__fixtures__/write-document.js.map +1 -0
  285. package/dist/lib/operations/cms-persistence.d.ts +124 -31
  286. package/dist/lib/operations/cms-persistence.js +447 -68
  287. package/dist/lib/operations/cms-persistence.js.map +1 -1
  288. package/dist/lib/operations/config-manifest-lookup.d.ts +35 -0
  289. package/dist/lib/operations/config-manifest-lookup.js +56 -0
  290. package/dist/lib/operations/config-manifest-lookup.js.map +1 -0
  291. package/dist/lib/operations/project-runtime-config.d.ts +39 -0
  292. package/dist/lib/operations/project-runtime-config.js +54 -0
  293. package/dist/lib/operations/project-runtime-config.js.map +1 -0
  294. package/dist/lib/operations/publish-with-revision.d.ts +64 -0
  295. package/dist/lib/operations/publish-with-revision.js +66 -0
  296. package/dist/lib/operations/publish-with-revision.js.map +1 -0
  297. package/dist/lib/operations/runtime-validator.d.ts +32 -0
  298. package/dist/lib/operations/runtime-validator.js +18 -0
  299. package/dist/lib/operations/runtime-validator.js.map +1 -0
  300. package/dist/lib/operations/session-hydration.d.ts +31 -14
  301. package/dist/lib/operations/session-hydration.js +12 -16
  302. package/dist/lib/operations/session-hydration.js.map +1 -1
  303. package/dist/lib/operations/studio-operations.d.ts +59 -2
  304. package/dist/lib/operations/studio-operations.js +76 -14
  305. package/dist/lib/operations/studio-operations.js.map +1 -1
  306. package/dist/lib/persistence/convex-adapter.d.ts +13 -5
  307. package/dist/lib/persistence/convex-adapter.js +189 -74
  308. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  309. package/dist/lib/persistence/convex-client.d.ts +65 -5
  310. package/dist/lib/persistence/convex-client.js +106 -3
  311. package/dist/lib/persistence/convex-client.js.map +1 -1
  312. package/dist/lib/persistence/index.js +17 -18
  313. package/dist/lib/persistence/index.js.map +1 -1
  314. package/dist/lib/persona/labels.js +4 -1
  315. package/dist/lib/persona/labels.js.map +1 -1
  316. package/dist/lib/query/operations-query.d.ts +1 -1
  317. package/dist/lib/query/operations-query.js +1 -1
  318. package/dist/lib/query/operations-query.js.map +1 -1
  319. package/dist/lib/references/impact.d.ts +21 -15
  320. package/dist/lib/references/impact.js +13 -26
  321. package/dist/lib/references/impact.js.map +1 -1
  322. package/dist/lib/references/references-store.d.ts +9 -0
  323. package/dist/lib/references/references-store.js +24 -4
  324. package/dist/lib/references/references-store.js.map +1 -1
  325. package/dist/lib/routes/route-issues.d.ts +6 -6
  326. package/dist/lib/routes/route-issues.js +26 -80
  327. package/dist/lib/routes/route-issues.js.map +1 -1
  328. package/dist/lib/workspace/config-issues.js +2 -1
  329. package/dist/lib/workspace/config-issues.js.map +1 -1
  330. package/dist/lib/workspace/index.d.ts +31 -1
  331. package/dist/lib/workspace/index.js +64 -17
  332. package/dist/lib/workspace/index.js.map +1 -1
  333. package/dist/lib/workspace/switcher-source.d.ts +3 -3
  334. package/dist/lib/workspace/switcher-source.js.map +1 -1
  335. package/dist/lib/workspace/workspace-source.d.ts +2 -2
  336. package/dist/lib/workspace/workspace-source.js +2 -1
  337. package/dist/lib/workspace/workspace-source.js.map +1 -1
  338. package/package.json +3 -3
  339. package/dist/components/cms/cms-composition-validators.d.ts +0 -16
  340. package/dist/components/cms/cms-composition-validators.js +0 -133
  341. package/dist/components/cms/cms-composition-validators.js.map +0 -1
  342. package/dist/components/cms/publish-clean.d.ts +0 -28
  343. package/dist/components/cms/publish-clean.js +0 -38
  344. package/dist/components/cms/publish-clean.js.map +0 -1
  345. package/dist/components/cms/use-save-mode.d.ts +0 -35
  346. package/dist/components/cms/use-save-mode.js +0 -54
  347. package/dist/components/cms/use-save-mode.js.map +0 -1
  348. package/dist/lib/assets/asset-placements.d.ts +0 -28
  349. package/dist/lib/assets/asset-placements.js +0 -83
  350. package/dist/lib/assets/asset-placements.js.map +0 -1
  351. package/dist/lib/cms/document-gate.d.ts +0 -47
  352. package/dist/lib/cms/document-gate.js +0 -61
  353. package/dist/lib/cms/document-gate.js.map +0 -1
@@ -6,7 +6,7 @@ import type { OperationError, OperationResult } from '@elytracms/studio/lib/oper
6
6
  *
7
7
  * Each selected document gets its OWN operations command
8
8
  * (`documents.publish` / `documents.unpublish` / `documents.delete`, EC-224;
9
- * `documents.upsert` for a set-field, EC-247), awaited in selection order, and
9
+ * one field patch for a set-field, EC-247 → EC-595 A2), awaited in selection order, and
10
10
  * its own per-item outcome. The report enumerates every failure with its
11
11
  * reason (validation gate, authorization denial, not-found, …) — a partial
12
12
  * failure is never silent and never collapsed into one toast. Session state is
@@ -61,22 +61,6 @@ export interface BulkContext {
61
61
  * when the action is `set-field`; unused otherwise.
62
62
  */
63
63
  applySetField?(ref: DocumentRef, set: BulkSetField): void;
64
- /**
65
- * EC-425: the publish-time hidden-strip (EC-273 clearHiddenValues + EC-394
66
- * clearHiddenProps, the SAME shared clean step the single-document `doPublish`
67
- * runs) — returns the document a publish should snapshot. Identity-preserving:
68
- * returns the input when nothing clears. Absent ⇒ publish the draft as stored
69
- * (fixture harnesses without a clean point).
70
- */
71
- cleanForPublish?(doc: CmsDocument): CmsDocument;
72
- /**
73
- * EC-425: land a cleaned draft (session + persistence) BEFORE its publish
74
- * command dispatches, so the snapshot the publish pins — and the reference
75
- * index derived from the stored draft (EC-286) — are clean. Awaited; only
76
- * called when {@link cleanForPublish} actually changed the document
77
- * (save-if-changed, mirroring `doPublish`).
78
- */
79
- applyCleaned?(ref: DocumentRef, doc: CmsDocument): Promise<void> | void;
80
64
  /**
81
65
  * Dispatch the ONE operations command for an item. Absent in fixture-only
82
66
  * sessions (no project scope) — items then mutate the session directly.
@@ -87,9 +71,9 @@ export interface BulkContext {
87
71
  * Run one bulk action over a selection, item by item:
88
72
  * 1. publish gates on validation — an invalid document fails with its issues
89
73
  * and dispatches NOTHING (a set-field is a draft edit, EC-225 — never gated),
90
- * 2. publish joins the single-document clean point (EC-425): the hidden-strip
91
- * runs per item and a changed draft is saved (awaited) BEFORE the publish
92
- * command, so bulk-published snapshots go out as clean as editor publishes,
74
+ * 2. the hidden-strip is NOT a step here (EC-585): it runs inside the one publish
75
+ * operation the dispatch calls, so a bulk-published snapshot is exactly as clean
76
+ * as an editor publish by construction (EC-425 used to run a clean step per item),
93
77
  * 3. each remaining item dispatches exactly one operations command, awaited,
94
78
  * 4. only succeeded items mutate the session,
95
79
  * 5. the report carries one honest outcome per item, in selection order.
@@ -25,9 +25,9 @@ const ACTION_STATE = {
25
25
  * Run one bulk action over a selection, item by item:
26
26
  * 1. publish gates on validation — an invalid document fails with its issues
27
27
  * and dispatches NOTHING (a set-field is a draft edit, EC-225 — never gated),
28
- * 2. publish joins the single-document clean point (EC-425): the hidden-strip
29
- * runs per item and a changed draft is saved (awaited) BEFORE the publish
30
- * command, so bulk-published snapshots go out as clean as editor publishes,
28
+ * 2. the hidden-strip is NOT a step here (EC-585): it runs inside the one publish
29
+ * operation the dispatch calls, so a bulk-published snapshot is exactly as clean
30
+ * as an editor publish by construction (EC-425 used to run a clean step per item),
31
31
  * 3. each remaining item dispatches exactly one operations command, awaited,
32
32
  * 4. only succeeded items mutate the session,
33
33
  * 5. the report carries one honest outcome per item, in selection order.
@@ -64,27 +64,6 @@ export async function runBulkAction(context, collectionId, ids, action, setField
64
64
  });
65
65
  continue;
66
66
  }
67
- // EC-425: the SAME clean-then-save sequence the single-document `doPublish`
68
- // runs, per item — validation gated on the live draft above (matching the
69
- // editor's pre-strip gate), then the hidden-strip; a changed draft lands
70
- // (awaited) before the publish command so the pinned snapshot and the
71
- // EC-286-derived reference index are clean. A failing save is an honest
72
- // per-item failure and the publish never dispatches on the dirty draft.
73
- if (context.cleanForPublish) {
74
- const cleaned = context.cleanForPublish(doc);
75
- if (cleaned !== doc) {
76
- try {
77
- await context.applyCleaned?.(ref, cleaned);
78
- }
79
- catch (cause) {
80
- fail({
81
- code: 'unknown',
82
- message: cause instanceof Error ? cause.message : String(cause),
83
- });
84
- continue;
85
- }
86
- }
87
- }
88
67
  }
89
68
  if (context.dispatch) {
90
69
  let result;
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-actions.js","sourceRoot":"","sources":["../../../src/components/cms/bulk-actions.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sCAAsC,CAAA;AA8C7C,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC5C,IAAI,SAAS,GAAG,CAAC,CAAA;IACjB,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO;QAAE,IAAI,MAAM,CAAC,EAAE;YAAE,SAAS,IAAI,CAAC,CAAA;IAClE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,SAAS,EAAE,CAAA;AACjE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,eAAe,CAAC,KAAqB;IACnD,4EAA4E;IAC5E,6EAA6E;IAC7E,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3D,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC9E,CAAC;IACD,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAA;AACrC,CAAC;AAwCD,MAAM,YAAY,GAAsD;IACtE,OAAO,EAAE,WAAW;IACpB,SAAS,EAAE,OAAO;CACnB,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAoB,EACpB,YAAoB,EACpB,GAAsB,EACtB,MAAkB,EAClB,QAAuB;IAEvB,MAAM,OAAO,GAAsB,EAAE,CAAA;IAErC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,MAAM,GAAG,GAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,EAAE,CAAA;QACzD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAClC,MAAM,IAAI,GAAG,CAAC,KAAqB,EAAE,EAAE,CACrC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;QAEhF,MAAM,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;QAC7D,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,CAAC;gBACH,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,SAAS,CAAC,kCAAkC,CAAC;gBACtD,MAAM,EAAE,cAAc;gBACtB,GAAG,EAAE,GAAG,YAAY,IAAI,EAAE,EAAE;aAC7B,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QAED,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;YAClF,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,IAAI,CAAC;oBACH,IAAI,EAAE,YAAY;oBAClB,OAAO,EAAE,SAAS,CAAC,uCAAuC,CAAC;oBAC3D,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;wBAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,8EAA8E;wBAC9E,OAAO,EAAE,yBAAyB,CAAC,KAAK,CAAC;wBACzC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC;wBACrB,MAAM,EAAE,KAAK;qBACd,CAAC,CAAC;iBACJ,CAAC,CAAA;gBACF,SAAQ;YACV,CAAC;YACD,4EAA4E;YAC5E,0EAA0E;YAC1E,yEAAyE;YACzE,sEAAsE;YACtE,wEAAwE;YACxE,wEAAwE;YACxE,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;gBAC5B,MAAM,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;gBAC5C,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;oBACpB,IAAI,CAAC;wBACH,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;oBAC5C,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,IAAI,CAAC;4BACH,IAAI,EAAE,SAAS;4BACf,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;yBAChE,CAAC,CAAA;wBACF,SAAQ;oBACV,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,IAAI,MAAgC,CAAA;YACpC,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YAC9C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,uEAAuE;gBACvE,6DAA6D;gBAC7D,MAAM,GAAG;oBACP,EAAE,EAAE,KAAK;oBACT,KAAK,EAAE;wBACL,IAAI,EAAE,SAAS;wBACf,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;qBAChE;iBACF,CAAA;YACH,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;gBACf,oEAAoE;gBACpE,gEAAgE;gBAChE,mEAAmE;gBACnE,yDAAyD;gBACzD,MAAM,gBAAgB,GAAG,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAA;gBACjF,IAAI,CAAC,gBAAgB,EAAE,CAAC;oBACtB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;oBAClB,SAAQ;gBACV,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,MAAM,KAAK,QAAQ;YAAE,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;aAC5C,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;YAChC,IAAI,QAAQ;gBAAE,OAAO,CAAC,aAAa,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;QACtD,CAAC;;YAAM,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAA;QACpD,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IACxC,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAA;AAC/D,CAAC","sourcesContent":["import type {\n CmsDocument,\n CmsValidationIssue,\n DocumentRef,\n PublicationState,\n} from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { matchesRef } from './document-ref-utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n cmsValidationIssueMessage,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Bulk actions over a table selection (EC-169) — the pure half.\n *\n * Each selected document gets its OWN operations command\n * (`documents.publish` / `documents.unpublish` / `documents.delete`, EC-224;\n * `documents.upsert` for a set-field, EC-247), awaited in selection order, and\n * its own per-item outcome. The report enumerates every failure with its\n * reason (validation gate, authorization denial, not-found, …) — a partial\n * failure is never silent and never collapsed into one toast. Session state is\n * only updated for items whose command succeeded.\n */\n\nexport type BulkAction = 'publish' | 'unpublish' | 'delete' | 'set-field'\n\n/**\n * The payload of a `set-field` bulk action (EC-247): assign one filterable\n * scalar field the same value across every selected document. The value is\n * folded into each document the locale-aware way (`applyCellValue`) by the\n * caller's `applySetField`/`dispatch`; this module only carries it.\n */\nexport interface BulkSetField {\n field: string\n value: FilterScalar\n}\n\n/** The outcome of one document's command within a bulk run. */\nexport interface BulkItemOutcome {\n ref: DocumentRef\n /** Display title at dispatch time (deleted docs still get named in the report). */\n title: string\n ok: boolean\n /** Human-readable failure reason — present exactly when `ok` is false. */\n reason?: string\n /** The structured error behind the reason (operation result / validation gate). */\n error?: OperationError\n}\n\nexport interface BulkReport {\n action: BulkAction\n /** Present for `set-field`: the field + value that was applied (report label). */\n setField?: BulkSetField\n results: BulkItemOutcome[]\n}\n\nexport function bulkSummary(report: BulkReport): { succeeded: number; failed: number } {\n let succeeded = 0\n for (const result of report.results) if (result.ok) succeeded += 1\n return { succeeded, failed: report.results.length - succeeded }\n}\n\n/** The failure reason a report row shows: validation issues beat the envelope. */\nexport function bulkErrorReason(error: OperationError): string {\n // EC-494: resolve the carried catalog refs — a bulk publish rejected by the\n // SERVER-side field-rule gate reports in the editor's language, not English.\n if (error.code === 'validation' && error.issues.length > 0) {\n return error.issues.map((issue) => operationIssueMessage(issue)).join(' · ')\n }\n return operationErrorMessage(error)\n}\n\nexport interface BulkContext {\n /** Current session documents (lookup + validation input). */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n /** Full document validation — gates publish: invalid documents never dispatch. */\n validate(doc: CmsDocument): CmsValidationIssue[]\n /** Apply one succeeded state transition to the session. */\n applyState(ref: DocumentRef, state: PublicationState): void\n /** Apply one succeeded delete to the session. */\n applyDelete(ref: DocumentRef): void\n /**\n * Apply one succeeded set-field to the session (locale-aware fold). Required\n * when the action is `set-field`; unused otherwise.\n */\n applySetField?(ref: DocumentRef, set: BulkSetField): void\n /**\n * EC-425: the publish-time hidden-strip (EC-273 clearHiddenValues + EC-394\n * clearHiddenProps, the SAME shared clean step the single-document `doPublish`\n * runs) — returns the document a publish should snapshot. Identity-preserving:\n * returns the input when nothing clears. Absent ⇒ publish the draft as stored\n * (fixture harnesses without a clean point).\n */\n cleanForPublish?(doc: CmsDocument): CmsDocument\n /**\n * EC-425: land a cleaned draft (session + persistence) BEFORE its publish\n * command dispatches, so the snapshot the publish pins — and the reference\n * index derived from the stored draft (EC-286) — are clean. Awaited; only\n * called when {@link cleanForPublish} actually changed the document\n * (save-if-changed, mirroring `doPublish`).\n */\n applyCleaned?(ref: DocumentRef, doc: CmsDocument): Promise<void> | void\n /**\n * Dispatch the ONE operations command for an item. Absent in fixture-only\n * sessions (no project scope) — items then mutate the session directly.\n */\n dispatch?: (action: BulkAction, ref: DocumentRef) => Promise<OperationResult<unknown>>\n}\n\nconst ACTION_STATE: Record<'publish' | 'unpublish', PublicationState> = {\n publish: 'published',\n unpublish: 'draft',\n}\n\n/**\n * Run one bulk action over a selection, item by item:\n * 1. publish gates on validation — an invalid document fails with its issues\n * and dispatches NOTHING (a set-field is a draft edit, EC-225 — never gated),\n * 2. publish joins the single-document clean point (EC-425): the hidden-strip\n * runs per item and a changed draft is saved (awaited) BEFORE the publish\n * command, so bulk-published snapshots go out as clean as editor publishes,\n * 3. each remaining item dispatches exactly one operations command, awaited,\n * 4. only succeeded items mutate the session,\n * 5. the report carries one honest outcome per item, in selection order.\n */\nexport async function runBulkAction(\n context: BulkContext,\n collectionId: string,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n const results: BulkItemOutcome[] = []\n\n for (const id of ids) {\n const ref: DocumentRef = { collection: collectionId, id }\n const title = context.titleOf(ref)\n const fail = (error: OperationError) =>\n results.push({ ref, title, ok: false, reason: bulkErrorReason(error), error })\n\n const doc = context.documents.find((d) => matchesRef(d, ref))\n if (!doc) {\n fail({\n code: 'not-found',\n message: translate('cms.list.bulk.error.notInSession'),\n entity: 'cms-document',\n key: `${collectionId}:${id}`,\n })\n continue\n }\n\n if (action === 'publish') {\n const errors = context.validate(doc).filter((issue) => issue.severity === 'error')\n if (errors.length > 0) {\n fail({\n code: 'validation',\n message: translate('cms.list.bulk.error.notValidToPublish'),\n issues: errors.map((issue) => ({\n code: issue.code,\n // EC-487 — resolved here, the last point that still has the structured issue.\n message: cmsValidationIssueMessage(issue),\n path: [...issue.path],\n domain: 'cms',\n })),\n })\n continue\n }\n // EC-425: the SAME clean-then-save sequence the single-document `doPublish`\n // runs, per item — validation gated on the live draft above (matching the\n // editor's pre-strip gate), then the hidden-strip; a changed draft lands\n // (awaited) before the publish command so the pinned snapshot and the\n // EC-286-derived reference index are clean. A failing save is an honest\n // per-item failure and the publish never dispatches on the dirty draft.\n if (context.cleanForPublish) {\n const cleaned = context.cleanForPublish(doc)\n if (cleaned !== doc) {\n try {\n await context.applyCleaned?.(ref, cleaned)\n } catch (cause) {\n fail({\n code: 'unknown',\n message: cause instanceof Error ? cause.message : String(cause),\n })\n continue\n }\n }\n }\n }\n\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(action, ref)\n } catch (cause) {\n // Operations never throw by contract; a broken transport still becomes\n // a visible per-item failure instead of an aborted bulk run.\n result = {\n ok: false,\n error: {\n code: 'unknown',\n message: cause instanceof Error ? cause.message : String(cause),\n },\n }\n }\n if (!result.ok) {\n // Idempotent delete: the document was never persisted (e.g. a draft\n // whose baseline upsert was rejected) or another editor already\n // deleted it. The desired end state — gone — holds, so removing it\n // from the session is the honest outcome, not a failure.\n const idempotentDelete = action === 'delete' && result.error.code === 'not-found'\n if (!idempotentDelete) {\n fail(result.error)\n continue\n }\n }\n }\n\n if (action === 'delete') context.applyDelete(ref)\n else if (action === 'set-field') {\n if (setField) context.applySetField?.(ref, setField)\n } else context.applyState(ref, ACTION_STATE[action])\n results.push({ ref, title, ok: true })\n }\n\n return { action, ...(setField ? { setField } : {}), results }\n}\n"]}
1
+ {"version":3,"file":"bulk-actions.js","sourceRoot":"","sources":["../../../src/components/cms/bulk-actions.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n CmsDocument,\n CmsValidationIssue,\n DocumentRef,\n PublicationState,\n} from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { matchesRef } from './document-ref-utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n cmsValidationIssueMessage,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Bulk actions over a table selection (EC-169) — the pure half.\n *\n * Each selected document gets its OWN operations command\n * (`documents.publish` / `documents.unpublish` / `documents.delete`, EC-224;\n * one field patch for a set-field, EC-247 → EC-595 A2), awaited in selection order, and\n * its own per-item outcome. The report enumerates every failure with its\n * reason (validation gate, authorization denial, not-found, …) — a partial\n * failure is never silent and never collapsed into one toast. Session state is\n * only updated for items whose command succeeded.\n */\n\nexport type BulkAction = 'publish' | 'unpublish' | 'delete' | 'set-field'\n\n/**\n * The payload of a `set-field` bulk action (EC-247): assign one filterable\n * scalar field the same value across every selected document. The value is\n * folded into each document the locale-aware way (`applyCellValue`) by the\n * caller's `applySetField`/`dispatch`; this module only carries it.\n */\nexport interface BulkSetField {\n field: string\n value: FilterScalar\n}\n\n/** The outcome of one document's command within a bulk run. */\nexport interface BulkItemOutcome {\n ref: DocumentRef\n /** Display title at dispatch time (deleted docs still get named in the report). */\n title: string\n ok: boolean\n /** Human-readable failure reason — present exactly when `ok` is false. */\n reason?: string\n /** The structured error behind the reason (operation result / validation gate). */\n error?: OperationError\n}\n\nexport interface BulkReport {\n action: BulkAction\n /** Present for `set-field`: the field + value that was applied (report label). */\n setField?: BulkSetField\n results: BulkItemOutcome[]\n}\n\nexport function bulkSummary(report: BulkReport): { succeeded: number; failed: number } {\n let succeeded = 0\n for (const result of report.results) if (result.ok) succeeded += 1\n return { succeeded, failed: report.results.length - succeeded }\n}\n\n/** The failure reason a report row shows: validation issues beat the envelope. */\nexport function bulkErrorReason(error: OperationError): string {\n // EC-494: resolve the carried catalog refs — a bulk publish rejected by the\n // SERVER-side field-rule gate reports in the editor's language, not English.\n if (error.code === 'validation' && error.issues.length > 0) {\n return error.issues.map((issue) => operationIssueMessage(issue)).join(' · ')\n }\n return operationErrorMessage(error)\n}\n\nexport interface BulkContext {\n /** Current session documents (lookup + validation input). */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n /** Full document validation — gates publish: invalid documents never dispatch. */\n validate(doc: CmsDocument): CmsValidationIssue[]\n /** Apply one succeeded state transition to the session. */\n applyState(ref: DocumentRef, state: PublicationState): void\n /** Apply one succeeded delete to the session. */\n applyDelete(ref: DocumentRef): void\n /**\n * Apply one succeeded set-field to the session (locale-aware fold). Required\n * when the action is `set-field`; unused otherwise.\n */\n applySetField?(ref: DocumentRef, set: BulkSetField): void\n /**\n * Dispatch the ONE operations command for an item. Absent in fixture-only\n * sessions (no project scope) — items then mutate the session directly.\n */\n dispatch?: (action: BulkAction, ref: DocumentRef) => Promise<OperationResult<unknown>>\n}\n\nconst ACTION_STATE: Record<'publish' | 'unpublish', PublicationState> = {\n publish: 'published',\n unpublish: 'draft',\n}\n\n/**\n * Run one bulk action over a selection, item by item:\n * 1. publish gates on validation — an invalid document fails with its issues\n * and dispatches NOTHING (a set-field is a draft edit, EC-225 — never gated),\n * 2. the hidden-strip is NOT a step here (EC-585): it runs inside the one publish\n * operation the dispatch calls, so a bulk-published snapshot is exactly as clean\n * as an editor publish by construction (EC-425 used to run a clean step per item),\n * 3. each remaining item dispatches exactly one operations command, awaited,\n * 4. only succeeded items mutate the session,\n * 5. the report carries one honest outcome per item, in selection order.\n */\nexport async function runBulkAction(\n context: BulkContext,\n collectionId: string,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n const results: BulkItemOutcome[] = []\n\n for (const id of ids) {\n const ref: DocumentRef = { collection: collectionId, id }\n const title = context.titleOf(ref)\n const fail = (error: OperationError) =>\n results.push({ ref, title, ok: false, reason: bulkErrorReason(error), error })\n\n const doc = context.documents.find((d) => matchesRef(d, ref))\n if (!doc) {\n fail({\n code: 'not-found',\n message: translate('cms.list.bulk.error.notInSession'),\n entity: 'cms-document',\n key: `${collectionId}:${id}`,\n })\n continue\n }\n\n if (action === 'publish') {\n const errors = context.validate(doc).filter((issue) => issue.severity === 'error')\n if (errors.length > 0) {\n fail({\n code: 'validation',\n message: translate('cms.list.bulk.error.notValidToPublish'),\n issues: errors.map((issue) => ({\n code: issue.code,\n // EC-487 — resolved here, the last point that still has the structured issue.\n message: cmsValidationIssueMessage(issue),\n path: [...issue.path],\n domain: 'cms',\n })),\n })\n continue\n }\n }\n\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(action, ref)\n } catch (cause) {\n // Operations never throw by contract; a broken transport still becomes\n // a visible per-item failure instead of an aborted bulk run.\n result = {\n ok: false,\n error: {\n code: 'unknown',\n message: cause instanceof Error ? cause.message : String(cause),\n },\n }\n }\n if (!result.ok) {\n // Idempotent delete: the document was never persisted (e.g. a draft\n // whose baseline upsert was rejected) or another editor already\n // deleted it. The desired end state — gone — holds, so removing it\n // from the session is the honest outcome, not a failure.\n const idempotentDelete = action === 'delete' && result.error.code === 'not-found'\n if (!idempotentDelete) {\n fail(result.error)\n continue\n }\n }\n }\n\n if (action === 'delete') context.applyDelete(ref)\n else if (action === 'set-field') {\n if (setField) context.applySetField?.(ref, setField)\n } else context.applyState(ref, ACTION_STATE[action])\n results.push({ ref, title, ok: true })\n }\n\n return { action, ...(setField ? { setField } : {}), results }\n}\n"]}
@@ -7,8 +7,8 @@ import type { OperationError, OperationResult } from '@elytracms/studio/lib/oper
7
7
  * Everything here is plain data + functions so the cell-edit state machine is
8
8
  * testable without a DOM: which cells are editable, how Tab/Enter advance,
9
9
  * how a draft value is folded into a document (active locale only), and how a
10
- * commit flows through exactly one `documents.upsert` operations command with
11
- * optimistic apply + rollback on rejection.
10
+ * commit flows through exactly one field patch (`documents.patch`, through the
11
+ * session's persister queue — EC-595 A2) with optimistic apply + rollback on rejection.
12
12
  */
13
13
  /** A cell in the collection table: one document × one field column. */
14
14
  export interface CellCoord {
@@ -90,9 +90,9 @@ export interface CellCommitContext extends CellEditOracles {
90
90
  /** Restore the pre-edit document after a rejected dispatch. */
91
91
  rollbackSession: (previous: CmsDocument) => void;
92
92
  /**
93
- * Dispatch the ONE operations command for this commit
94
- * (`documents.upsert`, EC-141). Absent in fixture-only sessions
95
- * (no project scope) — the commit is session-only then.
93
+ * Dispatch the ONE write for this commit — the field's patch through the persister's
94
+ * queue (`documents.patch`, EC-595 A2). Absent in fixture-only sessions (no project
95
+ * scope) — the commit is session-only then.
96
96
  */
97
97
  dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>;
98
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createDocumentStore, isFieldLocalized, validateDocument, writeField } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { gateValidateOptions } from './gate-validation'\nimport type { FindAsset } from './gate-validation'\nimport { anchorFieldName } from './inline-field-errors'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { replaceDoc } from './document-ref-utils'\nimport {\n cmsValidationIssueMessage,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Inline cell editing for the collection table (EC-168) — the pure half.\n *\n * Everything here is plain data + functions so the cell-edit state machine is\n * testable without a DOM: which cells are editable, how Tab/Enter advance,\n * how a draft value is folded into a document (active locale only), and how a\n * commit flows through exactly one `documents.upsert` operations command with\n * optimistic apply + rollback on rejection.\n */\n\n// --- Cell identity -----------------------------------------------------------\n\n/** A cell in the collection table: one document × one field column. */\nexport interface CellCoord {\n docId: string\n field: string\n}\n\n/** Stable key for a cell (state maps, DOM lookups). */\nexport function cellKey(coord: CellCoord): string {\n return `${coord.docId}::${coord.field}`\n}\n\n// --- Editability -------------------------------------------------------------\n\n/** How a cell edits: inline control, anchored picker dialog, or not at all. */\nexport type CellEditKind = 'inline' | 'picker' | 'none'\n\nconst INLINE_TYPES: ReadonlySet<FieldDef['type']> = new Set([\n 'text',\n 'number',\n 'boolean',\n 'select',\n 'date',\n])\n\n/**\n * Classify a field's in-table editing surface. Rich text is deliberately not\n * inline-editable (the detail editor owns it); read-only fields never edit.\n */\nexport function cellEditKind(field: FieldDef, readOnly: boolean): CellEditKind {\n if (readOnly) return 'none'\n if (field.type === 'relation' || field.type === 'asset') return 'picker'\n return INLINE_TYPES.has(field.type) ? 'inline' : 'none'\n}\n\n// --- Keyboard advance --------------------------------------------------------\n\n/** Where a committed edit moves next: Tab → next, Shift-Tab → prev, Enter → down. */\nexport type AdvanceDirection = 'next' | 'prev' | 'down'\n\n/**\n * The next editable cell in the table grid, or `null` at a boundary.\n * `docIds` is the rendered row order; `fields` the editable column order.\n * 'next'/'prev' walk row-major (wrapping across rows); 'down' stays in the\n * same column.\n */\nexport function nextEditableCell(\n docIds: readonly string[],\n fields: readonly string[],\n current: CellCoord,\n direction: AdvanceDirection,\n): CellCoord | null {\n const row = docIds.indexOf(current.docId)\n const col = fields.indexOf(current.field)\n if (row < 0 || col < 0 || fields.length === 0) return null\n if (direction === 'down') {\n const nextDoc = docIds[row + 1]\n return nextDoc === undefined ? null : { docId: nextDoc, field: current.field }\n }\n const flat = row * fields.length + col + (direction === 'next' ? 1 : -1)\n if (flat < 0 || flat >= docIds.length * fields.length) return null\n const docId = docIds[Math.floor(flat / fields.length)]!\n return { docId, field: fields[flat % fields.length]! }\n}\n\n// --- Applying a draft value ----------------------------------------------------\n\n// Folding one field value into a document — localized fields write ONLY the given\n// locale's bag (EC-018), shared fields the `values` bag, `undefined` clears — is\n// core's `writeField` (EC-580); this module's `applyCellValue` was a copy of it.\n\n// --- Validation gate -----------------------------------------------------------\n\n/**\n * Error-severity issues that the edited cell itself caused. Localized fields\n * only reject on issues for the edited locale — a missing translation in some\n * *other* locale must not block this commit.\n *\n * EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +\n * `localeConfig` subset it used to assemble here. A cell edit is not a weaker\n * KIND of validation — it is the same validation, FILTERED to the cell being\n * committed, which is the whole option-set contract in one function: surfaces\n * differ in what they do with the issues, never in what they validated with.\n *\n * EC-516: an issue belongs to the edited cell when the TOP-LEVEL field its\n * `path` resolves to is the edited column — `anchorFieldName`, the grammar the\n * document form's inline errors already use. `meta.field` is never compared\n * here: `validateObject` stamps it with the LEAF subfield name, so a broken\n * `items[0].title` used to pass for a `title` issue and block an unrelated\n * `title` edit (and the owning `items` column saw nothing).\n */\nexport function cellEditIssues(\n registry: CollectionRegistry,\n documents: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n collection: CollectionDef,\n next: CmsDocument,\n fieldName: string,\n locale: Locale,\n oracles: CellEditOracles = {},\n): CmsValidationIssue[] {\n // Validate against the session with the edited document swapped in, so\n // self-relations and same-document targets resolve.\n const store = createDocumentStore(replaceDoc(documents, next))\n const issues = validateDocument(\n registry,\n next,\n gateValidateOptions({\n store,\n localeConfig,\n isPublished: oracles.isPublished ?? (() => false),\n ...(oracles.findAsset ? { findAsset: oracles.findAsset } : {}),\n }),\n )\n const localized = isFieldLocalized(collection, fieldName)\n return issues.filter((issue) => {\n if (issue.severity !== 'error') return false\n if (anchorFieldName(collection, issue) !== fieldName) return false\n if (!localized) return true\n // Localized field paths are ['documents', key, 'localized', <locale>, <field>, …].\n const at = issue.path.indexOf('localized')\n return at < 0 || issue.path[at + 1] === locale\n })\n}\n\n// --- Commit pipeline -------------------------------------------------------------\n\n/** Outcome of one cell commit: the new document, or a structured rejection. */\nexport type CellCommitOutcome =\n | { ok: true; doc: CmsDocument }\n | { ok: false; error: OperationError }\n\nexport interface CellCommitInput {\n collection: CollectionDef\n /** The document as currently rendered (the rollback target). */\n doc: CmsDocument\n field: FieldDef\n /** The active locale at the point of edit (table locale resolution). */\n locale: Locale\n /** The committed draft value (`undefined` clears the field). */\n value: unknown\n}\n\n/**\n * The session oracles the canonical option bag needs beyond the store + locales\n * (EC-497). Optional so a fixture-only caller still gets the rest of the bag;\n * absent `isPublished` means \"nothing is published\", the engine's own default.\n */\nexport interface CellEditOracles {\n isPublished?: (ref: DocumentRef) => boolean\n /** The asset-library oracle (EC-588) — record lookup; absent = no asset checks. */\n findAsset?: FindAsset\n}\n\nexport interface CellCommitContext extends CellEditOracles {\n registry: CollectionRegistry\n /** Current session documents (relation/asset target resolution). */\n documents: readonly CmsDocument[]\n localeConfig: LocaleConfig\n /** Optimistically apply the edited document to the session. */\n applySession: (next: CmsDocument) => void\n /** Restore the pre-edit document after a rejected dispatch. */\n rollbackSession: (previous: CmsDocument) => void\n /**\n * Dispatch the ONE operations command for this commit\n * (`documents.upsert`, EC-141). Absent in fixture-only sessions\n * (no project scope) — the commit is session-only then.\n */\n dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>\n}\n\n/**\n * Commit one cell edit:\n * 1. fold the value into the document (active locale only),\n * 2. gate on field validation — rejected edits dispatch NOTHING and report a\n * structured validation error for the cell,\n * 3. apply optimistically, dispatch exactly one operations command,\n * 4. roll the session back when the operation result is a rejection.\n */\nexport async function commitCellEdit(\n context: CellCommitContext,\n input: CellCommitInput,\n): Promise<CellCommitOutcome> {\n const next = writeField(input.collection, input.doc, input.field.name, input.locale, input.value)\n\n const issues = cellEditIssues(\n context.registry,\n context.documents,\n context.localeConfig,\n input.collection,\n next,\n input.field.name,\n input.locale,\n {\n ...(context.isPublished ? { isPublished: context.isPublished } : {}),\n ...(context.findAsset ? { findAsset: context.findAsset } : {}),\n },\n )\n if (issues.length > 0) {\n return {\n ok: false,\n error: {\n code: 'validation',\n // EC-487/EC-494: this producer is CLIENT-side and already holds the structured\n // issue, so it resolves the message here. (Since EC-494 the wire also carries\n // the ref, which is what makes the SERVER-side publish gate localize.)\n message: cmsValidationIssueMessage(issues[0]!),\n issues: issues.map((issue) => ({\n code: issue.code,\n message: cmsValidationIssueMessage(issue),\n path: [...issue.path],\n domain: 'cms',\n })),\n },\n }\n }\n\n context.applySession(next)\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(next)\n } catch (cause) {\n // Operations never throw by contract; treat a broken transport the same\n // as a failed command so the cell still rolls back visibly.\n result = {\n ok: false,\n error: { code: 'unknown', message: cause instanceof Error ? cause.message : String(cause) },\n }\n }\n if (!result.ok) {\n context.rollbackSession(input.doc)\n return { ok: false, error: result.error }\n }\n }\n return { ok: true, doc: next }\n}\n\n/** The message the cell renders for a rejection: the first issue beats the envelope. */\nexport function cellErrorMessage(error: OperationError): string {\n // EC-494: an OperationIssue now carries its catalog ref across the wire, so a\n // rejection built SERVER-SIDE (the publish gate) localizes here too — not only the\n // studio-built ones above.\n if (error.code === 'validation' && error.issues.length > 0)\n return operationIssueMessage(error.issues[0]!)\n return operationErrorMessage(error)\n}\n"]}
1
+ {"version":3,"file":"cell-edit.js","sourceRoot":"","sources":["../../../src/components/cms/cell-edit.ts"],"names":[],"mappings":"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","sourcesContent":["import { createDocumentStore, isFieldLocalized, validateDocument, writeField } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n CollectionRegistry,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { gateValidateOptions } from './gate-validation'\nimport type { FindAsset } from './gate-validation'\nimport { anchorFieldName } from './inline-field-errors'\nimport type { OperationError, OperationResult } from '@elytracms/studio/lib/operations'\nimport { replaceDoc } from './document-ref-utils'\nimport {\n cmsValidationIssueMessage,\n operationErrorMessage,\n operationIssueMessage,\n} from '@elytracms/studio/lib/persona/labels'\n\n/**\n * Inline cell editing for the collection table (EC-168) — the pure half.\n *\n * Everything here is plain data + functions so the cell-edit state machine is\n * testable without a DOM: which cells are editable, how Tab/Enter advance,\n * how a draft value is folded into a document (active locale only), and how a\n * commit flows through exactly one field patch (`documents.patch`, through the\n * session's persister queue — EC-595 A2) with optimistic apply + rollback on rejection.\n */\n\n// --- Cell identity -----------------------------------------------------------\n\n/** A cell in the collection table: one document × one field column. */\nexport interface CellCoord {\n docId: string\n field: string\n}\n\n/** Stable key for a cell (state maps, DOM lookups). */\nexport function cellKey(coord: CellCoord): string {\n return `${coord.docId}::${coord.field}`\n}\n\n// --- Editability -------------------------------------------------------------\n\n/** How a cell edits: inline control, anchored picker dialog, or not at all. */\nexport type CellEditKind = 'inline' | 'picker' | 'none'\n\nconst INLINE_TYPES: ReadonlySet<FieldDef['type']> = new Set([\n 'text',\n 'number',\n 'boolean',\n 'select',\n 'date',\n])\n\n/**\n * Classify a field's in-table editing surface. Rich text is deliberately not\n * inline-editable (the detail editor owns it); read-only fields never edit.\n */\nexport function cellEditKind(field: FieldDef, readOnly: boolean): CellEditKind {\n if (readOnly) return 'none'\n if (field.type === 'relation' || field.type === 'asset') return 'picker'\n return INLINE_TYPES.has(field.type) ? 'inline' : 'none'\n}\n\n// --- Keyboard advance --------------------------------------------------------\n\n/** Where a committed edit moves next: Tab → next, Shift-Tab → prev, Enter → down. */\nexport type AdvanceDirection = 'next' | 'prev' | 'down'\n\n/**\n * The next editable cell in the table grid, or `null` at a boundary.\n * `docIds` is the rendered row order; `fields` the editable column order.\n * 'next'/'prev' walk row-major (wrapping across rows); 'down' stays in the\n * same column.\n */\nexport function nextEditableCell(\n docIds: readonly string[],\n fields: readonly string[],\n current: CellCoord,\n direction: AdvanceDirection,\n): CellCoord | null {\n const row = docIds.indexOf(current.docId)\n const col = fields.indexOf(current.field)\n if (row < 0 || col < 0 || fields.length === 0) return null\n if (direction === 'down') {\n const nextDoc = docIds[row + 1]\n return nextDoc === undefined ? null : { docId: nextDoc, field: current.field }\n }\n const flat = row * fields.length + col + (direction === 'next' ? 1 : -1)\n if (flat < 0 || flat >= docIds.length * fields.length) return null\n const docId = docIds[Math.floor(flat / fields.length)]!\n return { docId, field: fields[flat % fields.length]! }\n}\n\n// --- Applying a draft value ----------------------------------------------------\n\n// Folding one field value into a document — localized fields write ONLY the given\n// locale's bag (EC-018), shared fields the `values` bag, `undefined` clears — is\n// core's `writeField` (EC-580); this module's `applyCellValue` was a copy of it.\n\n// --- Validation gate -----------------------------------------------------------\n\n/**\n * Error-severity issues that the edited cell itself caused. Localized fields\n * only reject on issues for the edited locale — a missing translation in some\n * *other* locale must not block this commit.\n *\n * EC-497: validates with THE bag (`gateValidateOptions`), not the `store` +\n * `localeConfig` subset it used to assemble here. A cell edit is not a weaker\n * KIND of validation — it is the same validation, FILTERED to the cell being\n * committed, which is the whole option-set contract in one function: surfaces\n * differ in what they do with the issues, never in what they validated with.\n *\n * EC-516: an issue belongs to the edited cell when the TOP-LEVEL field its\n * `path` resolves to is the edited column — `anchorFieldName`, the grammar the\n * document form's inline errors already use. `meta.field` is never compared\n * here: `validateObject` stamps it with the LEAF subfield name, so a broken\n * `items[0].title` used to pass for a `title` issue and block an unrelated\n * `title` edit (and the owning `items` column saw nothing).\n */\nexport function cellEditIssues(\n registry: CollectionRegistry,\n documents: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n collection: CollectionDef,\n next: CmsDocument,\n fieldName: string,\n locale: Locale,\n oracles: CellEditOracles = {},\n): CmsValidationIssue[] {\n // Validate against the session with the edited document swapped in, so\n // self-relations and same-document targets resolve.\n const store = createDocumentStore(replaceDoc(documents, next))\n const issues = validateDocument(\n registry,\n next,\n gateValidateOptions({\n store,\n localeConfig,\n isPublished: oracles.isPublished ?? (() => false),\n ...(oracles.findAsset ? { findAsset: oracles.findAsset } : {}),\n }),\n )\n const localized = isFieldLocalized(collection, fieldName)\n return issues.filter((issue) => {\n if (issue.severity !== 'error') return false\n if (anchorFieldName(collection, issue) !== fieldName) return false\n if (!localized) return true\n // Localized field paths are ['documents', key, 'localized', <locale>, <field>, …].\n const at = issue.path.indexOf('localized')\n return at < 0 || issue.path[at + 1] === locale\n })\n}\n\n// --- Commit pipeline -------------------------------------------------------------\n\n/** Outcome of one cell commit: the new document, or a structured rejection. */\nexport type CellCommitOutcome =\n | { ok: true; doc: CmsDocument }\n | { ok: false; error: OperationError }\n\nexport interface CellCommitInput {\n collection: CollectionDef\n /** The document as currently rendered (the rollback target). */\n doc: CmsDocument\n field: FieldDef\n /** The active locale at the point of edit (table locale resolution). */\n locale: Locale\n /** The committed draft value (`undefined` clears the field). */\n value: unknown\n}\n\n/**\n * The session oracles the canonical option bag needs beyond the store + locales\n * (EC-497). Optional so a fixture-only caller still gets the rest of the bag;\n * absent `isPublished` means \"nothing is published\", the engine's own default.\n */\nexport interface CellEditOracles {\n isPublished?: (ref: DocumentRef) => boolean\n /** The asset-library oracle (EC-588) — record lookup; absent = no asset checks. */\n findAsset?: FindAsset\n}\n\nexport interface CellCommitContext extends CellEditOracles {\n registry: CollectionRegistry\n /** Current session documents (relation/asset target resolution). */\n documents: readonly CmsDocument[]\n localeConfig: LocaleConfig\n /** Optimistically apply the edited document to the session. */\n applySession: (next: CmsDocument) => void\n /** Restore the pre-edit document after a rejected dispatch. */\n rollbackSession: (previous: CmsDocument) => void\n /**\n * Dispatch the ONE write for this commit — the field's patch through the persister's\n * queue (`documents.patch`, EC-595 A2). Absent in fixture-only sessions (no project\n * scope) — the commit is session-only then.\n */\n dispatch?: (next: CmsDocument) => Promise<OperationResult<unknown>>\n}\n\n/**\n * Commit one cell edit:\n * 1. fold the value into the document (active locale only),\n * 2. gate on field validation — rejected edits dispatch NOTHING and report a\n * structured validation error for the cell,\n * 3. apply optimistically, dispatch exactly one operations command,\n * 4. roll the session back when the operation result is a rejection.\n */\nexport async function commitCellEdit(\n context: CellCommitContext,\n input: CellCommitInput,\n): Promise<CellCommitOutcome> {\n const next = writeField(input.collection, input.doc, input.field.name, input.locale, input.value)\n\n const issues = cellEditIssues(\n context.registry,\n context.documents,\n context.localeConfig,\n input.collection,\n next,\n input.field.name,\n input.locale,\n {\n ...(context.isPublished ? { isPublished: context.isPublished } : {}),\n ...(context.findAsset ? { findAsset: context.findAsset } : {}),\n },\n )\n if (issues.length > 0) {\n return {\n ok: false,\n error: {\n code: 'validation',\n // EC-487/EC-494: this producer is CLIENT-side and already holds the structured\n // issue, so it resolves the message here. (Since EC-494 the wire also carries\n // the ref, which is what makes the SERVER-side publish gate localize.)\n message: cmsValidationIssueMessage(issues[0]!),\n issues: issues.map((issue) => ({\n code: issue.code,\n message: cmsValidationIssueMessage(issue),\n path: [...issue.path],\n domain: 'cms',\n })),\n },\n }\n }\n\n context.applySession(next)\n if (context.dispatch) {\n let result: OperationResult<unknown>\n try {\n result = await context.dispatch(next)\n } catch (cause) {\n // Operations never throw by contract; treat a broken transport the same\n // as a failed command so the cell still rolls back visibly.\n result = {\n ok: false,\n error: { code: 'unknown', message: cause instanceof Error ? cause.message : String(cause) },\n }\n }\n if (!result.ok) {\n context.rollbackSession(input.doc)\n return { ok: false, error: result.error }\n }\n }\n return { ok: true, doc: next }\n}\n\n/** The message the cell renders for a rejection: the first issue beats the envelope. */\nexport function cellErrorMessage(error: OperationError): string {\n // EC-494: an OperationIssue now carries its catalog ref across the wire, so a\n // rejection built SERVER-SIDE (the publish gate) localizes here too — not only the\n // studio-built ones above.\n if (error.code === 'validation' && error.issues.length > 0)\n return operationIssueMessage(error.issues[0]!)\n return operationErrorMessage(error)\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"cell.js","sourceRoot":"","sources":["../../../src/components/cms/cell.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Boxes, Check, Image, Link2, Minus } from 'lucide-react'\nimport { readAssetId, readDocumentRef } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef, FieldDef } from '@elytracms/core/cms-core'\nimport { blockRootsOf } from '@elytracms/core/project-graph'\nimport { richTextPlainText } from '@elytracms/core/rich-text'\nimport { Badge, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { localeChipLabel, resolveDisplayedValue } from '@elytracms/studio/lib/locale'\nimport type { DisplayedFallback } from '@elytracms/studio/lib/locale'\nimport type { ResolvedAsset } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { isDisplayableAssetUrl } from '@elytracms/studio/lib/assets'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { AssetThumb, useAssetRecord } from '../assets/asset-thumb'\n\n/**\n * Render a document's field value for a table cell, by field type (brief\n * §8.4, EC-070), resolved in the studio's ambient active locale (EC-176).\n * A value served via EC-018 locale fallback carries a visible chip — never\n * silently mixed in as if it were translated.\n */\nexport function CellValue({\n collection,\n doc,\n field,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n field: FieldDef\n}) {\n const source = useCollectionView()\n const { value, fallback } = resolveDisplayedValue(\n collection,\n doc,\n field.name,\n source.activeLocale,\n source.localeConfig,\n )\n\n if (value === undefined || value === null || value === '') {\n return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n }\n\n return (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-1.5\">\n <TypedCellValue field={field} value={value} />\n <FallbackChip fallback={fallback} testId={`cell-fallback-${doc.id}-${field.name}`} />\n </span>\n )\n}\n\n/**\n * Subtle source-locale chip on a fallback-displayed value (EC-176): e.g. a\n * German view showing untranslated English content gets an \"EN\" marker.\n */\nexport function FallbackChip({\n fallback,\n testId,\n}: {\n fallback: DisplayedFallback | null\n testId: string\n}) {\n if (fallback === null) return null\n return (\n <Badge\n variant=\"outline\"\n data-testid={testId}\n title={fallbackTitle(fallback)}\n className=\"shrink-0 px-1 py-0 text-[0.6rem] uppercase tracking-wide text-muted-foreground\"\n >\n {localeChipLabel(fallback.sourceLocale)}\n </Badge>\n )\n}\n\n/** Plain-language tooltip of a fallback chip. */\nexport function fallbackTitle(fallback: DisplayedFallback): string {\n return translate('cms.cell.fallbackTitle', {\n source: localeChipLabel(fallback.sourceLocale),\n requested: localeChipLabel(fallback.requestedLocale),\n })\n}\n\nfunction TypedCellValue({ field, value }: { field: FieldDef; value: unknown }) {\n switch (field.type) {\n case 'boolean':\n return value === true ? (\n <Check className=\"size-4 text-success\" />\n ) : (\n <Minus className=\"size-3.5 text-muted-foreground/50\" />\n )\n case 'select':\n return <Badge variant=\"outline\">{String(value)}</Badge>\n case 'number':\n return <Text size=\"sm\" mono>{String(value)}</Text>\n case 'date':\n return (\n <Text size=\"sm\" tone=\"muted\" mono>\n {String(value).slice(0, 10)}\n </Text>\n )\n case 'richText':\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {richTextCellSummary(value).slice(0, 40) || translate('cms.cell.richText')}\n </Text>\n )\n case 'relation':\n return <RelationCell value={value} />\n case 'asset':\n return <AssetCell value={value} />\n case 'blocks':\n return <BlocksCell value={value} />\n case 'object': {\n // EC-253: list-view summary, never `[object Object]`. A repeater shows its\n // item count; a single object is edited in the detail form, not the cell.\n if (field.multiple) {\n const count = Array.isArray(value) ? value.length : 0\n return (\n <Badge variant=\"outline\">\n {translate(count === 1 ? 'cms.cell.itemCount.one' : 'cms.cell.itemCount.other', { count })}\n </Badge>\n )\n }\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {value && typeof value === 'object' ? translate('cms.cell.object') : '—'}\n </Text>\n )\n }\n default:\n return (\n <Text size=\"sm\" className=\"truncate\">\n {String(value)}\n </Text>\n )\n }\n}\n\nfunction RelationCell({ value }: { value: unknown }) {\n const source = useCollectionView()\n const refs = toRefArray(value)\n if (refs.length === 0) return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n // EC-246c: calm de-pilled read — a single muted link glyph + the title(s), not\n // a row of info pills. Matches the relation FIELD's row treatment; the pill was\n // a design cue the list doesn't need.\n return (\n <span className=\"inline-flex min-w-0 items-center gap-1.5 text-sm\">\n <Link2 className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">\n {refs.slice(0, 3).map((ref) => source.titleOf(ref)).join(', ')}\n </span>\n {refs.length > 3 ? (\n <Text size=\"xs\" tone=\"subtle\" className=\"shrink-0\">\n +{refs.length - 3}\n </Text>\n ) : null}\n </span>\n )\n}\n\n/**\n * A blocks/composition cell: a count of TOP-LEVEL blocks (EC-212) — matching\n * what the canvas/page-tree present as the block list, never `[object Object]`.\n */\nfunction BlocksCell({ value }: { value: unknown }) {\n const count = blockRootsOf(value).length\n if (count === 0) return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n return (\n <Badge variant=\"outline\">\n <Boxes className=\"size-3\" />{' '}\n {translate(count === 1 ? 'cms.cell.blockCount.one' : 'cms.cell.blockCount.other', { count })}\n </Badge>\n )\n}\n\nfunction AssetCell({ value }: { value: unknown }) {\n const source = useCollectionView()\n const assetCollectionId = source.assetCollectionId ?? 'asset'\n // EC-277: tolerate self-typed `{ type:'asset', id }` envelopes and legacy bare ids.\n const ids = (Array.isArray(value) ? value : [value])\n .map(readAssetId)\n .filter((id): id is string => id !== undefined)\n if (ids.length === 0) return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n // EC-346: multiple assets show the first, plus a \"+N\" count for the rest —\n // this IS the field cell, not a gallery editor.\n const first = ids[0]!\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <AssetFieldThumb assetId={first} assetCollectionId={assetCollectionId} />\n {ids.length > 1 ? <Text size=\"xs\" tone=\"subtle\">+{ids.length - 1}</Text> : null}\n </div>\n )\n}\n\n/**\n * EC-346 — an asset-type FIELD column's cell in a document list: a small\n * thumbnail (`size-8`, matching the 0.0.39 filename-column pattern), reusing the\n * SAME {@link AssetThumb} the assets rail uses so a focal point (EC-230/248)\n * crops the preview identically everywhere it appears. Resolves the full record\n * from the ambient asset library (`useAssetRecord` — ProjectProviders already\n * mounts an `<AssetsProvider>` at the project level, EC-195, so every CMS list\n * surface sees it) and prefers it over the source's own summary because only the\n * ambient record carries the focal point. Falls back to the source's\n * `resolveAsset` label/best-effort-image (the AD-19 \"no `<AssetsProvider>`\"\n * degradation a portable rail — or a test double — without a mounted library\n * still needs) so this never regresses the prior behaviour. Never crashes on an\n * id that resolves to nothing — both paths render a placeholder, never a badge\n * defaulted from a missing record.\n */\nfunction AssetFieldThumb({\n assetId,\n assetCollectionId,\n}: {\n assetId: string\n assetCollectionId: string\n}) {\n const source = useCollectionView()\n const ambient = useAssetRecord(assetId)\n if (ambient) return <AssetThumb record={ambient} className=\"size-8 shrink-0\" />\n const resolved = source.resolveAsset?.(assetId)\n const label =\n resolved?.title ?? resolved?.filename ?? source.titleOf({ collection: assetCollectionId, id: assetId })\n return <AssetPreview record={resolved} label={label} />\n}\n\n/**\n * The pre-EC-346 fallback preview: an image thumbnail for image assets with a\n * resolvable URL (EC-212), otherwise the labelled icon badge (and the same\n * badge whenever the source can't resolve the record — e.g. no asset backend).\n * Used only when no ambient asset library is mounted (see {@link AssetFieldThumb}).\n */\nfunction AssetPreview({ record, label }: { record: ResolvedAsset | undefined; label: string }) {\n const isImage = record?.contentType?.startsWith('image/') ?? false\n const url = record?.url\n if (isImage && isDisplayableAssetUrl(url)) {\n return (\n <span className=\"inline-flex min-w-0 items-center gap-1.5\">\n <img\n src={url}\n alt={record?.alt ?? label}\n loading=\"lazy\"\n className=\"size-7 shrink-0 rounded border border-border bg-muted object-cover\"\n />\n <Text size=\"xs\" tone=\"subtle\" className=\"max-w-[10rem] truncate\">\n {label}\n </Text>\n </span>\n )\n }\n return (\n <Badge variant=\"outline\">\n <Image className=\"size-3\" /> {label}\n </Badge>\n )\n}\n\n/**\n * Normalize a relation value (single ref or array of refs) into an array of\n * `DocumentRef`s. EC-277: unwraps self-typed `{ type:'reference', ... }` envelopes\n * and legacy bare `{ collection, id }` via the shared tolerant reader.\n */\nexport function toRefArray(value: unknown): DocumentRef[] {\n const list = Array.isArray(value) ? value : [value]\n return list.map(readDocumentRef).filter((ref): ref is DocumentRef => ref !== undefined)\n}\n\nfunction stripHtml(html: string): string {\n return html.replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()\n}\n\n/**\n * EC-348 — a richText field's stored value is the rich-text envelope\n * (`{ version, doc }` TipTap JSON, EC-314), not an HTML string: `stripHtml`\n * alone here yielded the literal `[object Object]`. Fallback chain: a valid\n * envelope extracts to plain text (`richTextPlainText`); a legacy plain/HTML\n * string (pre-EC-314 content, or any other non-envelope string) still goes\n * through `stripHtml` as before; anything else (garbage, wrong field-type data)\n * yields `''` so the cell's placeholder shows — never a crash, never\n * `[object Object]`.\n */\nfunction richTextCellSummary(value: unknown): string {\n const extracted = richTextPlainText(value)\n if (extracted !== undefined) return extracted\n return typeof value === 'string' ? stripHtml(value) : ''\n}\n"]}
1
+ {"version":3,"file":"cell.js","sourceRoot":"","sources":["../../../src/components/cms/cell.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAEvE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AAGrF,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAElE;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,EACxB,UAAU,EACV,GAAG,EACH,KAAK,GAKN;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,qBAAqB,CAC/C,UAAU,EACV,GAAG,EACH,KAAK,CAAC,IAAI,EACV,MAAM,CAAC,YAAY,EACnB,MAAM,CAAC,YAAY,CACpB,CAAA;IAED,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAC1D,OAAO,KAAC,KAAK,IAAC,SAAS,EAAC,mCAAmC,GAAG,CAAA;IAChE,CAAC;IAED,OAAO,CACL,gBAAM,SAAS,EAAC,qDAAqD,aACnE,KAAC,cAAc,IAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAI,EAC9C,KAAC,YAAY,IAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,iBAAiB,GAAG,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,GAAI,IAChF,CACR,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,QAAQ,EACR,MAAM,GAIP;IACC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAClC,OAAO,CACL,KAAC,KAAK,IACJ,OAAO,EAAC,SAAS,iBACJ,MAAM,EACnB,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,EAC9B,SAAS,EAAC,gFAAgF,YAEzF,eAAe,CAAC,QAAQ,CAAC,YAAY,CAAC,GACjC,CACT,CAAA;AACH,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,aAAa,CAAC,QAA2B;IACvD,OAAO,SAAS,CAAC,wBAAwB,EAAE;QACzC,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,YAAY,CAAC;QAC9C,SAAS,EAAE,eAAe,CAAC,QAAQ,CAAC,eAAe,CAAC;KACrD,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,EAAE,KAAK,EAAE,KAAK,EAAuC;IAC3E,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CACtB,KAAC,KAAK,IAAC,SAAS,EAAC,qBAAqB,GAAG,CAC1C,CAAC,CAAC,CAAC,CACF,KAAC,KAAK,IAAC,SAAS,EAAC,mCAAmC,GAAG,CACxD,CAAA;QACH,KAAK,QAAQ;YACX,OAAO,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,MAAM,CAAC,KAAK,CAAC,GAAS,CAAA;QACzD,KAAK,QAAQ;YACX,OAAO,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,kBAAE,MAAM,CAAC,KAAK,CAAC,GAAQ,CAAA;QACpD,KAAK,MAAM;YACT,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,kBAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GACtB,CACR,CAAA;QACH,KAAK,UAAU;YACb,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,YAC1B,mBAAmB,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,SAAS,CAAC,mBAAmB,CAAC,GACrE,CACR,CAAA;QACH,KAAK,UAAU;YACb,OAAO,KAAC,YAAY,IAAC,KAAK,EAAE,KAAK,GAAI,CAAA;QACvC,KAAK,OAAO;YACV,OAAO,KAAC,SAAS,IAAC,KAAK,EAAE,KAAK,GAAI,CAAA;QACpC,KAAK,QAAQ;YACX,OAAO,KAAC,UAAU,IAAC,KAAK,EAAE,KAAK,GAAI,CAAA;QACrC,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,2EAA2E;YAC3E,0EAA0E;YAC1E,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACnB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;gBACrD,OAAO,CACL,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YACrB,SAAS,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,CAAC,GACpF,CACT,CAAA;YACH,CAAC;YACD,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,YAC1B,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAAG,GACnE,CACR,CAAA;QACH,CAAC;QACD;YACE,OAAO,CACL,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,UAAU,YACjC,MAAM,CAAC,KAAK,CAAC,GACT,CACR,CAAA;IACL,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,EAAE,KAAK,EAAsB;IACjD,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAC9B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAC,KAAK,IAAC,SAAS,EAAC,mCAAmC,GAAG,CAAA;IACrF,+EAA+E;IAC/E,gFAAgF;IAChF,sCAAsC;IACtC,OAAO,CACL,gBAAM,SAAS,EAAC,kDAAkD,aAChE,KAAC,KAAK,IAAC,SAAS,EAAC,yCAAyC,GAAG,EAC7D,eAAM,SAAS,EAAC,kBAAkB,YAC/B,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GACzD,EACN,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACjB,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,UAAU,kBAC9C,IAAI,CAAC,MAAM,GAAG,CAAC,IACZ,CACR,CAAC,CAAC,CAAC,IAAI,IACH,CACR,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,UAAU,CAAC,EAAE,KAAK,EAAsB;IAC/C,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,MAAM,CAAA;IACxC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,KAAC,KAAK,IAAC,SAAS,EAAC,mCAAmC,GAAG,CAAA;IAC/E,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aACtB,KAAC,KAAK,IAAC,SAAS,EAAC,QAAQ,GAAG,EAAC,GAAG,EAC/B,SAAS,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAE,CAAC,IACtF,CACT,CAAA;AACH,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,KAAK,EAAsB;IAC9C,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,IAAI,OAAO,CAAA;IAC7D,oFAAoF;IACpF,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;SACjD,GAAG,CAAC,WAAW,CAAC;SAChB,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC,CAAA;IACjD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAC,KAAK,IAAC,SAAS,EAAC,mCAAmC,GAAG,CAAA;IACpF,2EAA2E;IAC3E,gDAAgD;IAChD,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,CAAE,CAAA;IACrB,OAAO,CACL,eAAK,SAAS,EAAC,qCAAqC,aAClD,KAAC,eAAe,IAAC,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,GAAI,EACxE,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,kBAAG,GAAG,CAAC,MAAM,GAAG,CAAC,IAAQ,CAAC,CAAC,CAAC,IAAI,IAC3E,CACP,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,eAAe,CAAC,EACvB,OAAO,EACP,iBAAiB,GAIlB;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;IACvC,IAAI,OAAO;QAAE,OAAO,KAAC,UAAU,IAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAC,iBAAiB,GAAG,CAAA;IAC/E,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC,OAAO,CAAC,CAAA;IAC/C,MAAM,KAAK,GACT,QAAQ,EAAE,KAAK,IAAI,QAAQ,EAAE,QAAQ,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,iBAAiB,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,CAAA;IACzG,OAAO,KAAC,YAAY,IAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,GAAI,CAAA;AACzD,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,EAAE,MAAM,EAAE,KAAK,EAAuD;IAC1F,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,EAAE,UAAU,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAA;IAClE,MAAM,GAAG,GAAG,MAAM,EAAE,GAAG,CAAA;IACvB,IAAI,OAAO,IAAI,qBAAqB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO,CACL,gBAAM,SAAS,EAAC,0CAA0C,aACxD,cACE,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,KAAK,EACzB,OAAO,EAAC,MAAM,EACd,SAAS,EAAC,oEAAoE,GAC9E,EACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,wBAAwB,YAC7D,KAAK,GACD,IACF,CACR,CAAA;IACH,CAAC;IACD,OAAO,CACL,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aACtB,KAAC,KAAK,IAAC,SAAS,EAAC,QAAQ,GAAG,OAAE,KAAK,IAC7B,CACT,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;IACnD,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAsB,EAAE,CAAC,GAAG,KAAK,SAAS,CAAC,CAAA;AACzF,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AAClE,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,mBAAmB,CAAC,KAAc;IACzC,MAAM,SAAS,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAA;IAC1C,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AAC1D,CAAC","sourcesContent":["import { Boxes, Check, Image, Link2, Minus } from 'lucide-react'\nimport { readAssetId, readDocumentRef } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, DocumentRef, FieldDef } from '@elytracms/core/cms-core'\nimport { blockRootsOf } from '@elytracms/core/project-graph'\nimport { richTextPlainText } from '@elytracms/core/rich-text'\nimport { Badge, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { localeChipLabel, resolveDisplayedValue } from '@elytracms/studio/lib/locale'\nimport type { DisplayedFallback } from '@elytracms/studio/lib/locale'\nimport type { AssetDisplay } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { isDisplayableAssetUrl } from '@elytracms/studio/lib/assets'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { AssetThumb, useAssetRecord } from '../assets/asset-thumb'\n\n/**\n * Render a document's field value for a table cell, by field type (brief\n * §8.4, EC-070), resolved in the studio's ambient active locale (EC-176).\n * A value served via EC-018 locale fallback carries a visible chip — never\n * silently mixed in as if it were translated.\n */\nexport function CellValue({\n collection,\n doc,\n field,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n field: FieldDef\n}) {\n const source = useCollectionView()\n const { value, fallback } = resolveDisplayedValue(\n collection,\n doc,\n field.name,\n source.activeLocale,\n source.localeConfig,\n )\n\n if (value === undefined || value === null || value === '') {\n return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n }\n\n return (\n <span className=\"inline-flex min-w-0 max-w-full items-center gap-1.5\">\n <TypedCellValue field={field} value={value} />\n <FallbackChip fallback={fallback} testId={`cell-fallback-${doc.id}-${field.name}`} />\n </span>\n )\n}\n\n/**\n * Subtle source-locale chip on a fallback-displayed value (EC-176): e.g. a\n * German view showing untranslated English content gets an \"EN\" marker.\n */\nexport function FallbackChip({\n fallback,\n testId,\n}: {\n fallback: DisplayedFallback | null\n testId: string\n}) {\n if (fallback === null) return null\n return (\n <Badge\n variant=\"outline\"\n data-testid={testId}\n title={fallbackTitle(fallback)}\n className=\"shrink-0 px-1 py-0 text-[0.6rem] uppercase tracking-wide text-muted-foreground\"\n >\n {localeChipLabel(fallback.sourceLocale)}\n </Badge>\n )\n}\n\n/** Plain-language tooltip of a fallback chip. */\nexport function fallbackTitle(fallback: DisplayedFallback): string {\n return translate('cms.cell.fallbackTitle', {\n source: localeChipLabel(fallback.sourceLocale),\n requested: localeChipLabel(fallback.requestedLocale),\n })\n}\n\nfunction TypedCellValue({ field, value }: { field: FieldDef; value: unknown }) {\n switch (field.type) {\n case 'boolean':\n return value === true ? (\n <Check className=\"size-4 text-success\" />\n ) : (\n <Minus className=\"size-3.5 text-muted-foreground/50\" />\n )\n case 'select':\n return <Badge variant=\"outline\">{String(value)}</Badge>\n case 'number':\n return <Text size=\"sm\" mono>{String(value)}</Text>\n case 'date':\n return (\n <Text size=\"sm\" tone=\"muted\" mono>\n {String(value).slice(0, 10)}\n </Text>\n )\n case 'richText':\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {richTextCellSummary(value).slice(0, 40) || translate('cms.cell.richText')}\n </Text>\n )\n case 'relation':\n return <RelationCell value={value} />\n case 'asset':\n return <AssetCell value={value} />\n case 'blocks':\n return <BlocksCell value={value} />\n case 'object': {\n // EC-253: list-view summary, never `[object Object]`. A repeater shows its\n // item count; a single object is edited in the detail form, not the cell.\n if (field.multiple) {\n const count = Array.isArray(value) ? value.length : 0\n return (\n <Badge variant=\"outline\">\n {translate(count === 1 ? 'cms.cell.itemCount.one' : 'cms.cell.itemCount.other', { count })}\n </Badge>\n )\n }\n return (\n <Text size=\"xs\" tone=\"subtle\">\n {value && typeof value === 'object' ? translate('cms.cell.object') : '—'}\n </Text>\n )\n }\n default:\n return (\n <Text size=\"sm\" className=\"truncate\">\n {String(value)}\n </Text>\n )\n }\n}\n\nfunction RelationCell({ value }: { value: unknown }) {\n const source = useCollectionView()\n const refs = toRefArray(value)\n if (refs.length === 0) return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n // EC-246c: calm de-pilled read — a single muted link glyph + the title(s), not\n // a row of info pills. Matches the relation FIELD's row treatment; the pill was\n // a design cue the list doesn't need.\n return (\n <span className=\"inline-flex min-w-0 items-center gap-1.5 text-sm\">\n <Link2 className=\"size-3.5 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">\n {refs.slice(0, 3).map((ref) => source.titleOf(ref)).join(', ')}\n </span>\n {refs.length > 3 ? (\n <Text size=\"xs\" tone=\"subtle\" className=\"shrink-0\">\n +{refs.length - 3}\n </Text>\n ) : null}\n </span>\n )\n}\n\n/**\n * A blocks/composition cell: a count of TOP-LEVEL blocks (EC-212) — matching\n * what the canvas/page-tree present as the block list, never `[object Object]`.\n */\nfunction BlocksCell({ value }: { value: unknown }) {\n const count = blockRootsOf(value).length\n if (count === 0) return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n return (\n <Badge variant=\"outline\">\n <Boxes className=\"size-3\" />{' '}\n {translate(count === 1 ? 'cms.cell.blockCount.one' : 'cms.cell.blockCount.other', { count })}\n </Badge>\n )\n}\n\nfunction AssetCell({ value }: { value: unknown }) {\n const source = useCollectionView()\n const assetCollectionId = source.assetCollectionId ?? 'asset'\n // EC-277: tolerate self-typed `{ type:'asset', id }` envelopes and legacy bare ids.\n const ids = (Array.isArray(value) ? value : [value])\n .map(readAssetId)\n .filter((id): id is string => id !== undefined)\n if (ids.length === 0) return <Minus className=\"size-3.5 text-muted-foreground/50\" />\n // EC-346: multiple assets show the first, plus a \"+N\" count for the rest —\n // this IS the field cell, not a gallery editor.\n const first = ids[0]!\n return (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <AssetFieldThumb assetId={first} assetCollectionId={assetCollectionId} />\n {ids.length > 1 ? <Text size=\"xs\" tone=\"subtle\">+{ids.length - 1}</Text> : null}\n </div>\n )\n}\n\n/**\n * EC-346 — an asset-type FIELD column's cell in a document list: a small\n * thumbnail (`size-8`, matching the 0.0.39 filename-column pattern), reusing the\n * SAME {@link AssetThumb} the assets rail uses so a focal point (EC-230/248)\n * crops the preview identically everywhere it appears. Resolves the full record\n * from the ambient asset library (`useAssetRecord` — ProjectProviders already\n * mounts an `<AssetsProvider>` at the project level, EC-195, so every CMS list\n * surface sees it) and prefers it over the source's own summary because only the\n * ambient record carries the focal point. Falls back to the source's\n * `resolveAsset` label/best-effort-image (the AD-19 \"no `<AssetsProvider>`\"\n * degradation a portable rail — or a test double — without a mounted library\n * still needs) so this never regresses the prior behaviour. Never crashes on an\n * id that resolves to nothing — both paths render a placeholder, never a badge\n * defaulted from a missing record.\n */\nfunction AssetFieldThumb({\n assetId,\n assetCollectionId,\n}: {\n assetId: string\n assetCollectionId: string\n}) {\n const source = useCollectionView()\n const ambient = useAssetRecord(assetId)\n if (ambient) return <AssetThumb record={ambient} className=\"size-8 shrink-0\" />\n const resolved = source.resolveAsset?.(assetId)\n const label =\n resolved?.title ?? resolved?.filename ?? source.titleOf({ collection: assetCollectionId, id: assetId })\n return <AssetPreview record={resolved} label={label} />\n}\n\n/**\n * The pre-EC-346 fallback preview: an image thumbnail for image assets with a\n * resolvable URL (EC-212), otherwise the labelled icon badge (and the same\n * badge whenever the source can't resolve the record — e.g. no asset backend).\n * Used only when no ambient asset library is mounted (see {@link AssetFieldThumb}).\n */\nfunction AssetPreview({ record, label }: { record: AssetDisplay | undefined; label: string }) {\n const isImage = record?.contentType?.startsWith('image/') ?? false\n const url = record?.url\n if (isImage && isDisplayableAssetUrl(url)) {\n return (\n <span className=\"inline-flex min-w-0 items-center gap-1.5\">\n <img\n src={url}\n alt={record?.alt ?? label}\n loading=\"lazy\"\n className=\"size-7 shrink-0 rounded border border-border bg-muted object-cover\"\n />\n <Text size=\"xs\" tone=\"subtle\" className=\"max-w-[10rem] truncate\">\n {label}\n </Text>\n </span>\n )\n }\n return (\n <Badge variant=\"outline\">\n <Image className=\"size-3\" /> {label}\n </Badge>\n )\n}\n\n/**\n * Normalize a relation value (single ref or array of refs) into an array of\n * `DocumentRef`s. EC-277: unwraps self-typed `{ type:'reference', ... }` envelopes\n * and legacy bare `{ collection, id }` via the shared tolerant reader.\n */\nexport function toRefArray(value: unknown): DocumentRef[] {\n const list = Array.isArray(value) ? value : [value]\n return list.map(readDocumentRef).filter((ref): ref is DocumentRef => ref !== undefined)\n}\n\nfunction stripHtml(html: string): string {\n return html.replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()\n}\n\n/**\n * EC-348 — a richText field's stored value is the rich-text envelope\n * (`{ version, doc }` TipTap JSON, EC-314), not an HTML string: `stripHtml`\n * alone here yielded the literal `[object Object]`. Fallback chain: a valid\n * envelope extracts to plain text (`richTextPlainText`); a legacy plain/HTML\n * string (pre-EC-314 content, or any other non-envelope string) still goes\n * through `stripHtml` as before; anything else (garbage, wrong field-type data)\n * yields `''` so the cell's placeholder shows — never a crash, never\n * `[object Object]`.\n */\nfunction richTextCellSummary(value: unknown): string {\n const extracted = richTextPlainText(value)\n if (extracted !== undefined) return extracted\n return typeof value === 'string' ? stripHtml(value) : ''\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope, ValidateDocumentOptions } from '@elytracms/core/cms-core';
3
- import type { DocumentVersionRecord } from '@elytracms/core/persistence';
4
- import type { OperationError } from '@elytracms/studio/lib/operations';
2
+ import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentPatch, DocumentRef, FieldDef, ValuePath, Locale, LocaleConfig, LocalizedString, RestoreScope, ValidateDocumentOptions } from '@elytracms/core/cms-core';
3
+ import type { CmsDocumentRecord, DocumentVersionRecord, IdFactory, PresenceEntry } from '@elytracms/core/persistence';
4
+ import type { CommitOutcome, DocumentConflict, OperationError, OperationsClient } from '@elytracms/studio/lib/operations';
5
5
  import type { TableViewState } from '@elytracms/studio/lib/cms-views';
6
6
  import type { CellCommitInput, CellCommitOutcome } from './cell-edit.js';
7
7
  import type { BulkAction, BulkReport, BulkSetField } from './bulk-actions.js';
@@ -54,22 +54,59 @@ export interface CmsStore {
54
54
  getDocument(ref: DocumentRef): CmsDocument | undefined;
55
55
  /** Resolve a relation/asset target's display title. */
56
56
  titleOf(ref: DocumentRef): string;
57
- saveDocument(doc: CmsDocument): void;
58
57
  /**
59
- * Commit ONE field edit from the table (EC-168): optimistic session apply +
60
- * exactly one `documents.upsert` operations command, awaited. Validation
61
- * failures and operation rejections return a structured error (the caller
62
- * renders it on the cell) and leave the session rolled back.
58
+ * EC-595 A2 — commit the patches a writer made to a document (the form's field
59
+ * paths, the canvas's node patches, an AI proposal's changes, …): the session's view
60
+ * updates at once and the patches persist through `documents.patch` against the
61
+ * revision the view was at. A refused path becomes a per-field conflict
62
+ * ({@link documentConflicts}); a failure, {@link persistError}. Resolves with the
63
+ * commit's outcome once acknowledged (fixture sessions: applied locally, at once).
64
+ */
65
+ commitPatches(ref: DocumentRef, patches: readonly DocumentPatch[], bases?: readonly (number | undefined)[]): Promise<CommitOutcome | undefined>;
66
+ /** EC-595 A2 — the revision the session's view of a document is at (the next patch's base). */
67
+ revisionOf(ref: DocumentRef): number | undefined;
68
+ /**
69
+ * EC-595 A2 / EC-578 — create a document from a whole value (duplicate, import of a
70
+ * new row, an AI create, a transfer, a reuse promote): insert-if-absent
71
+ * (`documents.replace` `{ ifAbsent: true }`), never an overwrite.
72
+ */
73
+ insertDocument(doc: CmsDocument): void;
74
+ /** EC-578 — a fresh document id from the session's injected id factory (UUIDs in the studio). */
75
+ newDocumentId(): string;
76
+ /**
77
+ * Commit ONE field edit from the table (EC-168): optimistic session apply + exactly
78
+ * one field patch through the persister's queue (EC-595 A2 — no longer a side
79
+ * channel past it), awaited. Validation failures and refusals return a structured
80
+ * error (the caller renders it on the cell).
63
81
  */
64
82
  commitFieldEdit(input: CellCommitInput): Promise<CellCommitOutcome>;
65
83
  createDocument(collectionId: string): DocumentRef;
66
84
  /**
67
- * Ensure a singleton collection's lone document exists and return its ref
68
- * (EC-274). Resolves the existing single document, or creates one with a normal
69
- * generated id (never the collection id). Idempotent. The singleton route opens
70
- * through this so the one document always exists to edit.
85
+ * Ensure a singleton collection's lone document exists (EC-274 → EC-595 A2): when the
86
+ * session holds none, `documents.create({ singleton: true })` — the store's
87
+ * `ifCollectionEmpty` precondition, so two sessions opening an empty singleton make
88
+ * ONE document; the loser adopts the winner's from the conflict. The singleton gate
89
+ * re-resolves once the document lands in the session.
90
+ */
91
+ ensureSingletonDocument(collectionId: string): void;
92
+ /** EC-595 A2 — the open per-field conflicts on a document ("changed by X — take theirs / keep mine"). */
93
+ documentConflicts(ref: DocumentRef): readonly DocumentConflict[];
94
+ /** EC-595 A2 — resolve one: `theirs` adopts the server value, `mine` re-sends at the current base. */
95
+ resolveConflict(ref: DocumentRef, conflictId: string, choice: 'theirs' | 'mine'): void;
96
+ /**
97
+ * EC-595 A2 (point 7) — adopt a row the open editor's document subscription pushed:
98
+ * applied under unflushed local edits, which win on their paths until flushed.
71
99
  */
72
- ensureSingletonDocument(collectionId: string): DocumentRef;
100
+ adoptDocumentSnapshot(ref: DocumentRef, record: CmsDocumentRecord | undefined): void;
101
+ /** EC-595 point 7 — follow one open document (rows to the persister, presence to the caller). */
102
+ subscribeDocument(ref: DocumentRef, onPresence: (presence: readonly PresenceEntry[]) => void): () => void;
103
+ /** EC-595 thin presence — this tab's heartbeat on a document (the focused field's path). */
104
+ reportPresence(ref: DocumentRef, path: ValuePath | undefined): void;
105
+ clearPresence(ref: DocumentRef): void;
106
+ /** This tab's presence id. */
107
+ presenceSessionId: string;
108
+ /** Write a whole document at the session's revision (a document-level key — the alias). */
109
+ replaceDocument(doc: CmsDocument): void;
73
110
  deleteDocument(ref: DocumentRef): void;
74
111
  /**
75
112
  * Is this document currently published? Sourced from the version POINTER
@@ -105,10 +142,11 @@ export interface CmsStore {
105
142
  */
106
143
  documentHasUnpublishedChanges(ref: DocumentRef): boolean;
107
144
  /**
108
- * Publish: snapshot the draft as a new version and pin it live (EC-224).
109
- * Flips published-ness optimistically, then reconciles against the backend
110
- * record (EC-225) so the badge can never claim a publish the backend refused
111
- * (e.g. blocked by publish-time field validation).
145
+ * Publish: the ONE publish operation (EC-585) — it clears the draft's hidden values,
146
+ * validates, and pins a new version (EC-224). Flips published-ness optimistically,
147
+ * then reconciles against the backend record (EC-225) so the badge can never claim a
148
+ * publish the backend refused — and a refusal REJECTS (`PublishRefusedError`, after
149
+ * reporting it on the persist banner), so no caller shows success for it.
112
150
  */
113
151
  publishDocument(ref: DocumentRef): Promise<void>;
114
152
  /**
@@ -182,41 +220,37 @@ export interface CmsStore {
182
220
  */
183
221
  persistError: OperationError | null;
184
222
  clearPersistError(): void;
185
- /** Seed the persisted baseline now (e.g. before applying an AI changeset). */
186
- ensurePersistedBaseline(): Promise<void>;
187
- /** Await all queued persisted mutations (tests, publish flows). */
188
- flushPersistence(): Promise<void>;
223
+ /**
224
+ * Await every queued persisted write (of one document, or all) — the persistence half
225
+ * of the FORCED FLUSH (publish, duplicate, transfer, an AI proposal, unmount, tab hide).
226
+ */
227
+ flushPersistence(ref?: DocumentRef): Promise<void>;
189
228
  }
190
229
  declare const useCms: () => CmsStore;
191
230
  declare function primaryField(collection: CollectionDef): string;
192
231
  /**
193
- * EC-316 (bug fix) — mint a fresh, COLLISION-FREE document id for `+ New`.
194
- *
195
- * `seq` is an in-memory, per-session counter (reset to 0 on every reload). A
196
- * document minted `new-<collection>-<n>` in a PRIOR session can already occupy
197
- * whatever `n` this session's counter would next reissue — the pre-fix bug:
198
- * `+ New` on a `post` collection that already held a (possibly published)
199
- * `new-post-1` handed out that SAME id again, silently opening the existing
200
- * document for editing instead of creating a fresh draft. Walking the counter
201
- * past every id already in `existingIds` makes the mint collision-free
202
- * regardless of what a prior session left behind.
232
+ * Build a fresh document for `collectionId` (EC-297 / EC-578): an id from THE injected
233
+ * id factory — UUIDs in the studio, so two sessions can never mint the same id (the old
234
+ * per-session `new-<collection>-<n>` counter minted `new-post-1` in both, and the second
235
+ * create overwrote the first) — and the collection's declared field defaults
236
+ * (`seedDefaults`; a localized default lands in the project's default-locale bucket,
237
+ * EC-580). Pure — unit-testable without a `CmsProvider`.
203
238
  */
204
- export declare function mintDocumentId(collectionId: string, seq: {
205
- current: number;
206
- }, existingIds: ReadonlySet<string>): string;
239
+ export declare function buildNewDocument(collectionId: string, registry: CollectionRegistry, newId: IdFactory, locales: LocaleConfig): CmsDocument;
207
240
  /**
208
- * Build a fresh document for `collectionId`: a collision-free id (EC-316, via
209
- * {@link mintDocumentId} against every id `existingDocs` already holds for
210
- * this collection) and the collection's declared field defaults (EC-297, via
211
- * `seedDefaults` — a localized default lands in the project's default-locale
212
- * bucket, EC-580). Pure — the sensitive part of the insert sequence shared by
213
- * `createDocument` and `ensureSingletonDocument`'s empty branch (via the
214
- * private `insertNewDocument` below), factored out so it's unit-testable
215
- * without a `CmsProvider`.
241
+ * EC-274 → EC-595 A2 (critic correction 9) — a singleton's first document:
242
+ * `documents.create({ singleton: true })` (the store's `ifCollectionEmpty`
243
+ * precondition). The race loser's `conflict` carries the winner's row, which it ADOPTS
244
+ * — two sessions opening an empty singleton end up on ONE document. Exported for tests.
216
245
  */
217
- export declare function buildNewDocument(collectionId: string, registry: CollectionRegistry, seq: {
218
- current: number;
219
- }, existingDocs: readonly CmsDocument[], locales: LocaleConfig): CmsDocument;
246
+ export declare function createOrAdoptSingleton(operations: OperationsClient, projectId: string, collectionId: string, seeded: Pick<CmsDocument, 'values' | 'localized'>): Promise<{
247
+ ok: true;
248
+ record: CmsDocumentRecord;
249
+ adopted: boolean;
250
+ } | {
251
+ ok: false;
252
+ error: OperationError;
253
+ }>;
220
254
  /**
221
255
  * Does `docs` already hold a document for `collectionId`? `ensureSingletonDocument`'s
222
256
  * empty branch re-checks this INSIDE the `setDocuments` updater (against the
@@ -278,6 +312,14 @@ export interface CmsProviderProps {
278
312
  * returned records. Absent (playground / kit) → empty set.
279
313
  */
280
314
  initialChangedKeys?: readonly string[];
315
+ /**
316
+ * EC-595 A2 — each document's hydrated revision (`documentKey` → revision): the base
317
+ * the session's patches are made against (the persister's `track`). Absent (fixture
318
+ * sessions) → a document is read once before its first patch.
319
+ */
320
+ initialRevisions?: Record<string, number>;
321
+ /** EC-578 — the document-id factory. Defaults to UUIDs; fixtures inject a sequential one. */
322
+ idFactory?: IdFactory;
281
323
  /**
282
324
  * Locale config for this session — the PROJECT's own (config-declared locales,
283
325
  * or its persisted settings; see `resolveCmsInitialLocaleConfig`). Absent → the