@elytracms/studio 0.0.87 → 0.0.90

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 (164) hide show
  1. package/convex/asset-revalidate.test.ts +35 -42
  2. package/convex/delivery.ts +8 -11
  3. package/convex/document-revalidate.test.ts +175 -0
  4. package/convex/redirect-revalidate.test.ts +36 -36
  5. package/convex/revalidate.ts +18 -30
  6. package/convex/triggers.ts +47 -38
  7. package/convex/webhooks.ts +77 -62
  8. package/dist/components/account/self-account-view-source.js +2 -1
  9. package/dist/components/account/self-account-view-source.js.map +1 -1
  10. package/dist/components/ai/ai-chat-panel.js +9 -2
  11. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  12. package/dist/components/app-views/default-modules.js +3 -3
  13. package/dist/components/app-views/default-modules.js.map +1 -1
  14. package/dist/components/assets/asset-grid.js +1 -1
  15. package/dist/components/assets/asset-grid.js.map +1 -1
  16. package/dist/components/assets/assets-collection-view-source.js +3 -2
  17. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  18. package/dist/components/assets/assets-store.d.ts +11 -1
  19. package/dist/components/assets/assets-store.js +10 -0
  20. package/dist/components/assets/assets-store.js.map +1 -1
  21. package/dist/components/assets/focal-point.d.ts +0 -7
  22. package/dist/components/assets/focal-point.js +0 -10
  23. package/dist/components/assets/focal-point.js.map +1 -1
  24. package/dist/components/builder/canvas-render-context.d.ts +12 -1
  25. package/dist/components/builder/canvas-render-context.js +20 -16
  26. package/dist/components/builder/canvas-render-context.js.map +1 -1
  27. package/dist/components/builder/reuse-context.js +2 -2
  28. package/dist/components/builder/reuse-context.js.map +1 -1
  29. package/dist/components/builder/reuse.d.ts +6 -2
  30. package/dist/components/builder/reuse.js +12 -7
  31. package/dist/components/builder/reuse.js.map +1 -1
  32. package/dist/components/cms/asset-field.d.ts +2 -1
  33. package/dist/components/cms/asset-field.js +27 -59
  34. package/dist/components/cms/asset-field.js.map +1 -1
  35. package/dist/components/cms/asset-picker-core.d.ts +77 -40
  36. package/dist/components/cms/asset-picker-core.js +73 -46
  37. package/dist/components/cms/asset-picker-core.js.map +1 -1
  38. package/dist/components/cms/asset-picker.d.ts +48 -19
  39. package/dist/components/cms/asset-picker.js +69 -30
  40. package/dist/components/cms/asset-picker.js.map +1 -1
  41. package/dist/components/cms/cell-edit.d.ts +3 -7
  42. package/dist/components/cms/cell-edit.js +7 -25
  43. package/dist/components/cms/cell-edit.js.map +1 -1
  44. package/dist/components/cms/cms-store.d.ts +3 -2
  45. package/dist/components/cms/cms-store.js +38 -47
  46. package/dist/components/cms/cms-store.js.map +1 -1
  47. package/dist/components/cms/collection-table.js +14 -4
  48. package/dist/components/cms/collection-table.js.map +1 -1
  49. package/dist/components/cms/detail-editor.js +20 -21
  50. package/dist/components/cms/detail-editor.js.map +1 -1
  51. package/dist/components/cms/duplicate-document.d.ts +3 -2
  52. package/dist/components/cms/duplicate-document.js +5 -5
  53. package/dist/components/cms/duplicate-document.js.map +1 -1
  54. package/dist/components/cms/editable-cell.js +3 -6
  55. package/dist/components/cms/editable-cell.js.map +1 -1
  56. package/dist/components/cms/field-control.js +7 -6
  57. package/dist/components/cms/field-control.js.map +1 -1
  58. package/dist/components/cms/gate-validation.d.ts +27 -2
  59. package/dist/components/cms/gate-validation.js +19 -1
  60. package/dist/components/cms/gate-validation.js.map +1 -1
  61. package/dist/components/cms/publish-clean.d.ts +13 -6
  62. package/dist/components/cms/publish-clean.js +14 -6
  63. package/dist/components/cms/publish-clean.js.map +1 -1
  64. package/dist/components/cms/rich-text/editor.js +2 -2
  65. package/dist/components/cms/rich-text/editor.js.map +1 -1
  66. package/dist/components/cms/rich-text/insert.d.ts +7 -0
  67. package/dist/components/cms/rich-text/insert.js +7 -0
  68. package/dist/components/cms/rich-text/insert.js.map +1 -1
  69. package/dist/components/cms/use-publish-controller.js +3 -2
  70. package/dist/components/cms/use-publish-controller.js.map +1 -1
  71. package/dist/components/components/transfer-dialog.js +1 -0
  72. package/dist/components/components/transfer-dialog.js.map +1 -1
  73. package/dist/components/components/transfer.d.ts +10 -4
  74. package/dist/components/components/transfer.js +11 -9
  75. package/dist/components/components/transfer.js.map +1 -1
  76. package/dist/components/delivery-health-banner.d.ts +3 -2
  77. package/dist/components/delivery-health-banner.js +3 -2
  78. package/dist/components/delivery-health-banner.js.map +1 -1
  79. package/dist/components/nav-issues.d.ts +23 -3
  80. package/dist/components/nav-issues.js +38 -2
  81. package/dist/components/nav-issues.js.map +1 -1
  82. package/dist/components/redirects/redirects-collection-view-source.js +3 -2
  83. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  84. package/dist/components/section-nav.js +26 -2
  85. package/dist/components/section-nav.js.map +1 -1
  86. package/dist/components/users/users-collection-view-source.js +2 -1
  87. package/dist/components/users/users-collection-view-source.js.map +1 -1
  88. package/dist/lib/ai/apply-content.js +32 -28
  89. package/dist/lib/ai/apply-content.js.map +1 -1
  90. package/dist/lib/ai/tools.d.ts +10 -4
  91. package/dist/lib/ai/tools.js +12 -4
  92. package/dist/lib/ai/tools.js.map +1 -1
  93. package/dist/lib/ai/translate-document.js +11 -7
  94. package/dist/lib/ai/translate-document.js.map +1 -1
  95. package/dist/lib/ai/types.d.ts +9 -5
  96. package/dist/lib/ai/types.js.map +1 -1
  97. package/dist/lib/ai/update-fields.js +10 -29
  98. package/dist/lib/ai/update-fields.js.map +1 -1
  99. package/dist/lib/ai/vite.d.ts +22 -2
  100. package/dist/lib/ai/vite.js +12 -3
  101. package/dist/lib/ai/vite.js.map +1 -1
  102. package/dist/lib/cms/published-documents.d.ts +14 -3
  103. package/dist/lib/cms/published-documents.js +12 -3
  104. package/dist/lib/cms/published-documents.js.map +1 -1
  105. package/dist/lib/cms-views/view-model.d.ts +4 -4
  106. package/dist/lib/cms-views/view-model.js +16 -13
  107. package/dist/lib/cms-views/view-model.js.map +1 -1
  108. package/dist/lib/data/collection-content.d.ts +22 -1
  109. package/dist/lib/data/collection-content.js +51 -14
  110. package/dist/lib/data/collection-content.js.map +1 -1
  111. package/dist/lib/delivery/instant-publish.d.ts +0 -3
  112. package/dist/lib/delivery/instant-publish.js +0 -1
  113. package/dist/lib/delivery/instant-publish.js.map +1 -1
  114. package/dist/lib/engine-version.d.ts +3 -3
  115. package/dist/lib/engine-version.js +3 -3
  116. package/dist/lib/engine-version.js.map +1 -1
  117. package/dist/lib/i18n/messages.de.js +5 -0
  118. package/dist/lib/i18n/messages.de.js.map +1 -1
  119. package/dist/lib/i18n/messages.en.d.ts +4 -0
  120. package/dist/lib/i18n/messages.en.js +6 -0
  121. package/dist/lib/i18n/messages.en.js.map +1 -1
  122. package/dist/lib/locale/fallback.d.ts +5 -19
  123. package/dist/lib/locale/fallback.js +18 -39
  124. package/dist/lib/locale/fallback.js.map +1 -1
  125. package/dist/lib/locale/locale-store.js +2 -1
  126. package/dist/lib/locale/locale-store.js.map +1 -1
  127. package/dist/lib/operations/session-hydration.d.ts +24 -26
  128. package/dist/lib/operations/session-hydration.js +15 -53
  129. package/dist/lib/operations/session-hydration.js.map +1 -1
  130. package/dist/lib/operations/studio-operations.js +4 -6
  131. package/dist/lib/operations/studio-operations.js.map +1 -1
  132. package/dist/lib/persistence/backend-mode.d.ts +5 -25
  133. package/dist/lib/persistence/backend-mode.js +13 -30
  134. package/dist/lib/persistence/backend-mode.js.map +1 -1
  135. package/dist/lib/references/reference-model.d.ts +17 -3
  136. package/dist/lib/references/reference-model.js +17 -23
  137. package/dist/lib/references/reference-model.js.map +1 -1
  138. package/dist/lib/references/references-store.js +7 -0
  139. package/dist/lib/references/references-store.js.map +1 -1
  140. package/dist/lib/workspace/config-issues.d.ts +6 -0
  141. package/dist/lib/workspace/config-issues.js +8 -4
  142. package/dist/lib/workspace/config-issues.js.map +1 -1
  143. package/dist/lib/workspace/index.d.ts +1 -8
  144. package/dist/lib/workspace/index.js +1 -10
  145. package/dist/lib/workspace/index.js.map +1 -1
  146. package/dist/router/navigate-once.d.ts +19 -4
  147. package/dist/router/navigate-once.js +22 -5
  148. package/dist/router/navigate-once.js.map +1 -1
  149. package/package.json +3 -7
  150. package/dist/lib/webhooks/config.d.ts +0 -15
  151. package/dist/lib/webhooks/config.js +0 -65
  152. package/dist/lib/webhooks/config.js.map +0 -1
  153. package/dist/lib/webhooks/dispatcher.d.ts +0 -70
  154. package/dist/lib/webhooks/dispatcher.js +0 -61
  155. package/dist/lib/webhooks/dispatcher.js.map +0 -1
  156. package/dist/lib/webhooks/index.d.ts +0 -11
  157. package/dist/lib/webhooks/index.js +0 -10
  158. package/dist/lib/webhooks/index.js.map +0 -1
  159. package/dist/lib/webhooks/signing.d.ts +0 -15
  160. package/dist/lib/webhooks/signing.js +0 -24
  161. package/dist/lib/webhooks/signing.js.map +0 -1
  162. package/dist/lib/webhooks/tags.d.ts +0 -25
  163. package/dist/lib/webhooks/tags.js +0 -289
  164. package/dist/lib/webhooks/tags.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n ExpandableSurface,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { AssetThumb } from '../assets/asset-thumb'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport {\n ASSET_PICKER_STAGE_PRESET,\n commitPickerSelection,\n commitPickerSelectionMany,\n uploadBatchProgress,\n visiblePickerAssets,\n} from './asset-picker-core'\nimport type { UploadBatch } from './asset-picker-core'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field.\n *\n * EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}\n * `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the\n * old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the\n * assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and\n * the upload-then-select path. It is a SELECTION browser, not a form — click = pick\n * (fires `onChange`, closes) plus the shipped clear/cancel affordances; the\n * forms-never-modal rule governs document forms and is untouched. Opened from the\n * expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer\n * stack — Escape closes the browser first, the rich-text stage stays.\n *\n * The component CONTRACT is unchanged (`{ open, onOpenChange, value, accept,\n * onChange }`) so every call site — asset fields, rich-text image insertion,\n * editable cells — works with zero edits, just a bigger surface. EC-460 adds an\n * OPTIONAL multi-commit pair (`multiple` + `onChangeMany`): a `many` asset field\n * passes both so uploading N files in ONE picker interaction attaches ALL N\n * (the old flow committed the first upload to land and closed — the rest\n * reached the library but never the field, silent partial data loss). Grid\n * clicks stay single-pick-and-close in every mode.\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** EC-460: commit a whole settled upload batch (a `many` field passes this). */\n onChangeMany?: (assetIds: string[]) => void\n /** Whether the consuming field holds many assets (enables batch commit). */\n multiple?: boolean\n /** The file input's `accept` attribute for in-picker upload + grid filter (field-derived). */\n accept?: string\n}) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n </AssetsProvider>\n )\n}\n\n/** The open media-browser stage over a mounted assets store. */\nfunction AssetPickerStage({\n value,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n onChangeMany?: (assetIds: string[]) => void\n multiple?: boolean\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // The active upload BATCH (EC-460): pre-upload library snapshot + how many\n // files the interaction submitted, so the effect below can attach the WHOLE\n // batch (the `upload()` API returns nothing — the library diff is the signal).\n const pendingRef = useRef<UploadBatch | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = visiblePickerAssets(store.assets, query, accept)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n // Uploads still running (failed entries are terminal — they stay in the queue\n // for retry, and must not keep a batch waiting forever).\n const uploadsInFlight = store.uploads.filter((u) => u.state.phase !== 'failed').length\n\n // Auto-select what an in-picker upload added, then close. A `many` consumer\n // (multiple + onChangeMany) waits for the batch to SETTLE and commits every\n // added id — uploading 2 files attaches 2 (EC-460); a single consumer keeps\n // the first-to-land commit.\n useEffect(() => {\n const batch = pendingRef.current\n if (!batch) return\n const progress = uploadBatchProgress(batch, libraryIds, uploadsInFlight)\n if (multiple && onChangeMany) {\n if (!progress.settled || progress.added.length === 0) return\n pendingRef.current = null\n commitPickerSelectionMany(progress.added, { onChangeMany, onOpenChange })\n return\n }\n const picked = progress.added[0] ?? null\n if (picked === null) return\n pendingRef.current = null\n commitPickerSelection(picked, { onChange, onOpenChange })\n }, [libraryIds, uploadsInFlight, multiple, onChange, onChangeMany, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = { before: new Set(libraryIds), expected: files.length }\n store.upload(files)\n }\n // The stage portals to document.body, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the stage would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <ExpandableSurface\n title={translate('cms.asset.pickTitle')}\n closeLabel={translate('common.close')}\n variant=\"stage\"\n preset={ASSET_PICKER_STAGE_PRESET}\n onDismiss={() => onOpenChange(false)}\n headerLeading={<ImageIcon className=\"size-4 text-muted-foreground\" aria-hidden />}\n >\n <div\n className=\"relative flex min-h-0 flex-1 flex-col gap-3 p-4\"\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className=\"grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\"\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim() ? translate('cms.asset.noMatches') : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => commitPickerSelection(asset.id, { onChange, onOpenChange })}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => commitPickerSelection(null, { onChange, onOpenChange })}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-lg border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </div>\n </ExpandableSurface>\n )\n}\n\n/** One selectable asset: EC-400 thumb tile + title/filename + alt text. The\n * internal asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n aria-pressed={selected}\n >\n <span className=\"relative block\">\n <AssetThumb record={asset} className=\"aspect-video w-full\" />\n {selected ? (\n <span className=\"absolute right-1 top-1 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow\">\n <Check className=\"size-3\" />\n </span>\n ) : null}\n </span>\n <Text size=\"xs\" className=\"min-w-0 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n"]}
1
+ {"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Search, Upload } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport { assetAcceptAttr, describeAccept } from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n ExpandableSurface,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { AssetThumb } from '../assets/asset-thumb'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport type { AssetsStore } from '../assets/assets-store'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport {\n ASSET_PICKER_STAGE_PRESET,\n commitPickerSelection,\n commitPickerSelectionMany,\n splitByAccept,\n uploadBatchProgress,\n visiblePickerAssets,\n} from './asset-picker-core'\nimport type { UploadBatch } from './asset-picker-core'\n\n/** The picker's props — see {@link AssetPicker}. */\nexport interface AssetPickerProps {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** EC-460: commit a whole settled upload batch (a `many` field passes this). */\n onChangeMany?: (assetIds: string[]) => void\n /** Whether the consuming field holds many assets (enables batch commit). */\n multiple?: boolean\n /** The field's `accept` list (EC-588) — absent means any file. */\n accept?: readonly string[]\n}\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field.\n *\n * EC-432 (AD-C): the picker is a real MEDIA BROWSER on the {@link ExpandableSurface}\n * `stage` (`grid` preset — `min(1240px, 94vw)`, near-full height), replacing the\n * old `max-w-md` (448px) Dialog. It reuses the shipped assets-rail machinery: the\n * assets store, the EC-152 search, the EC-400 thumb tile ({@link AssetThumb}), and\n * the upload-then-select path. It is a SELECTION browser, not a form — click = pick\n * (fires `onChange`, closes) plus the shipped clear/cancel affordances; the\n * forms-never-modal rule governs document forms and is untouched. Opened from the\n * expanded rich-text stage toolbar it rides ABOVE that stage on the EC-392 layer\n * stack — Escape closes the browser first, the rich-text stage stays.\n *\n * The component CONTRACT (`{ open, onOpenChange, value, accept, onChange }`) serves\n * every call site — asset fields, rich-text image insertion, editable cells. EC-460\n * adds an OPTIONAL multi-commit pair (`multiple` + `onChangeMany`): a `many` asset\n * field passes both so uploading N files in ONE picker interaction attaches ALL N\n * (the old flow committed the first upload to land and closed — the rest reached the\n * library but never the field, silent partial data loss). Grid clicks stay\n * single-pick-and-close in every mode.\n *\n * EC-588: `accept` is the field's STRUCTURED list (it was a file-input string that the\n * core re-split — two shapes for one fact). The grid filter, the file-input attribute\n * and the drop/upload split all derive from it, and an absent `accept` is unrestricted\n * everywhere (the Upload dialog used to fall back to `image/*`). A field-backed caller\n * passes `assetPickerBinding`'s output (`asset-picker-core`) rather than wiring props\n * by hand.\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: AssetPickerProps) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerStage\n value={value}\n onOpenChange={onOpenChange}\n onChange={onChange}\n {...(onChangeMany ? { onChangeMany } : {})}\n multiple={multiple}\n accept={accept}\n />\n </AssetsProvider>\n )\n}\n\n/** The open media-browser stage over a mounted assets store. */\nfunction AssetPickerStage(props: Omit<AssetPickerProps, 'open'>) {\n const store = useAssets()\n return (\n <ExpandableSurface\n title={translate('cms.asset.pickTitle')}\n closeLabel={translate('common.close')}\n variant=\"stage\"\n preset={ASSET_PICKER_STAGE_PRESET}\n onDismiss={() => props.onOpenChange(false)}\n headerLeading={<ImageIcon className=\"size-4 text-muted-foreground\" aria-hidden />}\n >\n <AssetPickerBrowser store={store} {...props} />\n </ExpandableSurface>\n )\n}\n\n/** The slice of the assets store the browser reads and drives. */\nexport type AssetPickerStore = Pick<\n AssetsStore,\n 'status' | 'errorMessage' | 'assets' | 'uploads' | 'upload' | 'landedAssetId'\n>\n\n/**\n * The media browser itself — search, Upload, the grid, the drop zone — rendered inside\n * the stage. A named component of its own (EC-588) so its markup is reachable by a\n * node-env smoke render (the stage portal is `null` there): the file input's `accept`\n * attribute and the grid's filter are pinned against the field's `accept`.\n */\nexport function AssetPickerBrowser({\n store,\n value,\n onOpenChange,\n onChange,\n onChangeMany,\n multiple = false,\n accept,\n}: Omit<AssetPickerProps, 'open'> & { store: AssetPickerStore }) {\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [dragging, setDragging] = useState(false)\n // Names of files a drop/upload refused against the field's `accept` (EC-588) — shown\n // as a visible notice and never uploaded. Cleared by the next upload attempt.\n const [rejected, setRejected] = useState<string[]>([])\n const fileInputRef = useRef<HTMLInputElement>(null)\n // The active upload BATCH (EC-460): exactly the upload ids this interaction started\n // (EC-588 A7), so the effect below attaches this batch's uploads and nothing else.\n const pendingRef = useRef<UploadBatch | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = visiblePickerAssets(store.assets, query, accept)\n\n // Auto-select what an in-picker upload added, then close. A `many` consumer\n // (multiple + onChangeMany) waits for the batch to SETTLE and commits every\n // added id — uploading 2 files attaches 2 (EC-460); a single consumer keeps\n // the first-to-land commit. Only uploads whose record satisfies `accept` commit.\n useEffect(() => {\n const batch = pendingRef.current\n if (!batch) return\n const progress = uploadBatchProgress(batch, store, accept)\n if (multiple && onChangeMany) {\n if (!progress.settled) return\n pendingRef.current = null\n if (progress.added.length > 0) {\n commitPickerSelectionMany(progress.added, { onChangeMany, onOpenChange })\n }\n return\n }\n const picked = progress.added[0] ?? null\n if (picked === null) {\n if (progress.settled) pendingRef.current = null // everything failed — disarm\n return\n }\n pendingRef.current = null\n commitPickerSelection(picked, { onChange, onOpenChange })\n }, [store, accept, multiple, onChange, onChangeMany, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n // EC-588: the drop rule — the same split the field's drop zone uses. A drop\n // bypasses the dialog's soft filter; refused files are named, never uploaded.\n const { accepted, rejected: refused } = splitByAccept(files, accept)\n setRejected(refused)\n if (accepted.length === 0) return\n pendingRef.current = { uploadIds: store.upload(accepted) }\n }\n // The stage portals to document.body, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the stage would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <div\n className=\"relative flex min-h-0 flex-1 flex-col gap-3 p-4\"\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={assetAcceptAttr(accept)}\n className=\"hidden\"\n data-testid=\"asset-picker-file-input\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <AssetRejectedNotice rejected={rejected} accept={accept} />\n\n <div\n className=\"grid min-h-0 flex-1 auto-rows-min grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5\"\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim() ? translate('cms.asset.noMatches') : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => commitPickerSelection(asset.id, { onChange, onOpenChange })}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => commitPickerSelection(null, { onChange, onOpenChange })}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-lg border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </div>\n )\n}\n\n/**\n * The visible notice for files an upload path refused against the field's `accept`\n * (EC-301 → EC-588): named, never silently dropped. Shared by the field's drop zone and\n * the picker, which apply the same rule. Renders nothing when nothing was refused.\n */\nexport function AssetRejectedNotice({\n rejected,\n accept,\n}: {\n rejected: readonly string[]\n accept: readonly string[] | undefined\n}) {\n if (rejected.length === 0) return null\n return (\n <Row gap=\"2\" align=\"center\" data-testid=\"asset-rejected-notice\">\n <AlertTriangle className=\"size-4 shrink-0 text-warning\" />\n <Text size=\"xs\" tone=\"muted\">\n {rejected.length === 1\n ? translate('cms.asset.rejected.one', { name: rejected[0]!, types: describeAccept(accept) })\n : translate('cms.asset.rejected.other', {\n count: rejected.length,\n types: describeAccept(accept),\n })}\n </Text>\n </Row>\n )\n}\n\n/** One selectable asset: EC-400 thumb tile + title/filename + alt text. The\n * internal asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n aria-pressed={selected}\n >\n <span className=\"relative block\">\n <AssetThumb record={asset} className=\"aspect-video w-full\" />\n {selected ? (\n <span className=\"absolute right-1 top-1 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow\">\n <Check className=\"size-3\" />\n </span>\n ) : null}\n </span>\n <Text size=\"xs\" className=\"min-w-0 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig } from '@elytracms/core/cms-core';
2
+ import type { FindAsset } from './gate-validation.js';
2
3
  import type { OperationError, OperationResult } from '@elytracms/studio/lib/operations';
3
4
  /**
4
5
  * Inline cell editing for the collection table (EC-168) — the pure half.
@@ -32,12 +33,6 @@ export type AdvanceDirection = 'next' | 'prev' | 'down';
32
33
  * same column.
33
34
  */
34
35
  export declare function nextEditableCell(docIds: readonly string[], fields: readonly string[], current: CellCoord, direction: AdvanceDirection): CellCoord | null;
35
- /**
36
- * Fold one field value into a document. Localized fields write ONLY the given
37
- * locale's bag (other locale variants untouched, EC-018); non-localized fields
38
- * write the shared `values` bag. `undefined` clears the entry.
39
- */
40
- export declare function applyCellValue(collection: CollectionDef, doc: CmsDocument, fieldName: string, locale: Locale, value: unknown): CmsDocument;
41
36
  /**
42
37
  * Error-severity issues that the edited cell itself caused. Localized fields
43
38
  * only reject on issues for the edited locale — a missing translation in some
@@ -82,7 +77,8 @@ export interface CellCommitInput {
82
77
  */
83
78
  export interface CellEditOracles {
84
79
  isPublished?: (ref: DocumentRef) => boolean;
85
- assetExists?: (id: string) => boolean;
80
+ /** The asset-library oracle (EC-588) — record lookup; absent = no asset checks. */
81
+ findAsset?: FindAsset;
86
82
  }
87
83
  export interface CellCommitContext extends CellEditOracles {
88
84
  registry: CollectionRegistry;
@@ -1,4 +1,4 @@
1
- import { createDocumentStore, isFieldLocalized, validateDocument } from '@elytracms/core/cms-core';
1
+ import { createDocumentStore, isFieldLocalized, validateDocument, writeField } from '@elytracms/core/cms-core';
2
2
  import { gateValidateOptions } from './gate-validation.js';
3
3
  import { anchorFieldName } from './inline-field-errors.js';
4
4
  import { replaceDoc } from './document-ref-utils.js';
@@ -47,27 +47,9 @@ export function nextEditableCell(docIds, fields, current, direction) {
47
47
  return { docId, field: fields[flat % fields.length] };
48
48
  }
49
49
  // --- Applying a draft value ----------------------------------------------------
50
- /**
51
- * Fold one field value into a document. Localized fields write ONLY the given
52
- * locale's bag (other locale variants untouched, EC-018); non-localized fields
53
- * write the shared `values` bag. `undefined` clears the entry.
54
- */
55
- export function applyCellValue(collection, doc, fieldName, locale, value) {
56
- if (isFieldLocalized(collection, fieldName)) {
57
- const perLocale = { ...(doc.localized?.[locale] ?? {}) };
58
- if (value === undefined)
59
- delete perLocale[fieldName];
60
- else
61
- perLocale[fieldName] = value;
62
- return { ...doc, localized: { ...doc.localized, [locale]: perLocale } };
63
- }
64
- const values = { ...doc.values };
65
- if (value === undefined)
66
- delete values[fieldName];
67
- else
68
- values[fieldName] = value;
69
- return { ...doc, values };
70
- }
50
+ // Folding one field value into a document — localized fields write ONLY the given
51
+ // locale's bag (EC-018), shared fields the `values` bag, `undefined` clears — is
52
+ // core's `writeField` (EC-580); this module's `applyCellValue` was a copy of it.
71
53
  // --- Validation gate -----------------------------------------------------------
72
54
  /**
73
55
  * Error-severity issues that the edited cell itself caused. Localized fields
@@ -95,7 +77,7 @@ export function cellEditIssues(registry, documents, localeConfig, collection, ne
95
77
  store,
96
78
  localeConfig,
97
79
  isPublished: oracles.isPublished ?? (() => false),
98
- ...(oracles.assetExists ? { assetExists: oracles.assetExists } : {}),
80
+ ...(oracles.findAsset ? { findAsset: oracles.findAsset } : {}),
99
81
  }));
100
82
  const localized = isFieldLocalized(collection, fieldName);
101
83
  return issues.filter((issue) => {
@@ -119,10 +101,10 @@ export function cellEditIssues(registry, documents, localeConfig, collection, ne
119
101
  * 4. roll the session back when the operation result is a rejection.
120
102
  */
121
103
  export async function commitCellEdit(context, input) {
122
- const next = applyCellValue(input.collection, input.doc, input.field.name, input.locale, input.value);
104
+ const next = writeField(input.collection, input.doc, input.field.name, input.locale, input.value);
123
105
  const issues = cellEditIssues(context.registry, context.documents, context.localeConfig, input.collection, next, input.field.name, input.locale, {
124
106
  ...(context.isPublished ? { isPublished: context.isPublished } : {}),
125
- ...(context.assetExists ? { assetExists: context.assetExists } : {}),
107
+ ...(context.findAsset ? { findAsset: context.findAsset } : {}),
126
108
  });
127
109
  if (issues.length > 0) {
128
110
  return {
@@ -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 } 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 { 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/**\n * Fold one field value into a document. Localized fields write ONLY the given\n * locale's bag (other locale variants untouched, EC-018); non-localized fields\n * write the shared `values` bag. `undefined` clears the entry.\n */\nexport function applyCellValue(\n collection: CollectionDef,\n doc: CmsDocument,\n fieldName: string,\n locale: Locale,\n value: unknown,\n): CmsDocument {\n if (isFieldLocalized(collection, fieldName)) {\n const perLocale = { ...(doc.localized?.[locale] ?? {}) }\n if (value === undefined) delete perLocale[fieldName]\n else perLocale[fieldName] = value\n return { ...doc, localized: { ...doc.localized, [locale]: perLocale } }\n }\n const values = { ...doc.values }\n if (value === undefined) delete values[fieldName]\n else values[fieldName] = value\n return { ...doc, values }\n}\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.assetExists ? { assetExists: oracles.assetExists } : {}),\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 assetExists?: (id: string) => boolean\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 = applyCellValue(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.assetExists ? { assetExists: context.assetExists } : {}),\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 `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"]}
@@ -208,14 +208,15 @@ export declare function mintDocumentId(collectionId: string, seq: {
208
208
  * Build a fresh document for `collectionId`: a collision-free id (EC-316, via
209
209
  * {@link mintDocumentId} against every id `existingDocs` already holds for
210
210
  * this collection) and the collection's declared field defaults (EC-297, via
211
- * `seedDefaults`). Pure — the sensitive part of the insert sequence shared by
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
212
213
  * `createDocument` and `ensureSingletonDocument`'s empty branch (via the
213
214
  * private `insertNewDocument` below), factored out so it's unit-testable
214
215
  * without a `CmsProvider`.
215
216
  */
216
217
  export declare function buildNewDocument(collectionId: string, registry: CollectionRegistry, seq: {
217
218
  current: number;
218
- }, existingDocs: readonly CmsDocument[]): CmsDocument;
219
+ }, existingDocs: readonly CmsDocument[], locales: LocaleConfig): CmsDocument;
219
220
  /**
220
221
  * Does `docs` already hold a document for `collectionId`? `ensureSingletonDocument`'s
221
222
  * empty branch re-checks this INSIDE the `setDocuments` updater (against the
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, resolveField, seedDefaults, resolveSingletonDocument, titleFieldOf, validateDocument, } from '@elytracms/core/cms-core';
3
+ import { createCollectionRegistry, createDocumentStore, documentKey, documentReachabilityIssues, documentSchema, resolveDocumentTitle, seedDefaults, resolveSingletonDocument, titleFieldOf, validateDocument, valuesInLocale, writeField, } from '@elytracms/core/cms-core';
4
4
  import { translate } from '@elytracms/studio/lib/i18n';
5
5
  import { configureStudioLocales, useStudioLocale } from '@elytracms/studio/lib/locale';
6
6
  import { studioManifestLookup } from '@elytracms/studio/lib/registry';
7
7
  import { resolveDeploymentCollections, resolveDeploymentLocaleConfig, resolveDeploymentRoutes, } from '@elytracms/studio/lib/workspace';
8
8
  import { Heading, Panel, Stack, Text } from '@elytracms/studio/ui';
9
- import { gateValidateOptions } from './gate-validation.js';
9
+ import { assetLibraryOracle, gateValidateOptions } from './gate-validation.js';
10
10
  import { clearSessionDocumentValidator, setSessionDocumentValidator, } from '@elytracms/studio/lib/cms/document-gate';
11
11
  import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
12
12
  import { createRequiredContext } from '@elytracms/studio/lib/required-context';
@@ -14,7 +14,7 @@ import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/o
14
14
  import { ALL_VIEW_ID, DEFAULT_VIEW_OVERRIDE_ID, canonicalViewState, configViewIssues, defaultConfigViewId, defaultViewState, deprecatedDefaultFlagViewIds, hasDefaultViewOverride, pseudoColumns, resolveCollectionViews, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
15
15
  import { useOptionalAssets } from '../assets/assets-store.js';
16
16
  import { consumeStagedDocuments } from './document-staging.js';
17
- import { applyCellValue, commitCellEdit } from './cell-edit.js';
17
+ import { commitCellEdit } from './cell-edit.js';
18
18
  import { runBulkAction } from './bulk-actions.js';
19
19
  import { cleanHiddenForPublish } from './publish-clean.js';
20
20
  const { Context: CmsContext, useRequired: useCms } = createRequiredContext('Cms');
@@ -45,16 +45,21 @@ export function mintDocumentId(collectionId, seq, existingIds) {
45
45
  * Build a fresh document for `collectionId`: a collision-free id (EC-316, via
46
46
  * {@link mintDocumentId} against every id `existingDocs` already holds for
47
47
  * this collection) and the collection's declared field defaults (EC-297, via
48
- * `seedDefaults`). Pure — the sensitive part of the insert sequence shared by
48
+ * `seedDefaults` — a localized default lands in the project's default-locale
49
+ * bucket, EC-580). Pure — the sensitive part of the insert sequence shared by
49
50
  * `createDocument` and `ensureSingletonDocument`'s empty branch (via the
50
51
  * private `insertNewDocument` below), factored out so it's unit-testable
51
52
  * without a `CmsProvider`.
52
53
  */
53
- export function buildNewDocument(collectionId, registry, seq, existingDocs) {
54
+ export function buildNewDocument(collectionId, registry, seq, existingDocs, locales) {
54
55
  const existingIds = new Set(existingDocs.filter((d) => d.collection === collectionId).map((d) => d.id));
55
56
  const id = mintDocumentId(collectionId, seq, existingIds);
56
57
  const collection = registry.get(collectionId);
57
- return { collection: collectionId, id, values: collection ? seedDefaults(collection) : {} };
58
+ return {
59
+ collection: collectionId,
60
+ id,
61
+ ...(collection ? seedDefaults(collection, locales) : { values: {} }),
62
+ };
58
63
  }
59
64
  /**
60
65
  * Does `docs` already hold a document for `collectionId`? `ensureSingletonDocument`'s
@@ -311,21 +316,13 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
311
316
  const registryRef = useRef(registry);
312
317
  registryRef.current = registry;
313
318
  const store = useMemo(() => createDocumentStore(documents), [documents]);
314
- // Asset existence is validated against the REAL asset library (assets live in
315
- // their own repository, not as CMS documents). With the ambient AssetsProvider
316
- // present, the oracle is the set of uploaded asset ids — but only once loaded;
317
- // while it loads we are optimistic so a valid upload is never transiently
318
- // flagged "unknown". With no provider (tests/fixtures), the oracle is absent
319
- // and asset existence falls back to the document store.
319
+ // Asset references are validated against the REAL asset library (assets live in
320
+ // their own repository, not as CMS documents). EC-588: the oracle returns the
321
+ // RECORD, so one lookup answers existence AND the field's `accept`. It is absent —
322
+ // no asset checks — with no provider mounted and while the library loads or
323
+ // errors (`assetLibraryOracle`), so a valid upload is never transiently flagged.
320
324
  const assetStore = useOptionalAssets();
321
- const assetExists = useMemo(() => {
322
- if (!assetStore)
323
- return undefined;
324
- if (assetStore.status !== 'ready')
325
- return () => true;
326
- const ids = new Set(assetStore.assets.map((asset) => asset.id));
327
- return (id) => ids.has(id);
328
- }, [assetStore]);
325
+ const findAsset = useMemo(() => assetLibraryOracle(assetStore), [assetStore]);
329
326
  // The ambient studio locale (EC-176): this session's locale config drives
330
327
  // the switcher options; every CMS surface follows the one active locale.
331
328
  // EC-479: the ambient store is `null` until this effect installs the project's
@@ -344,21 +341,12 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
344
341
  const collection = registry.get(ref.collection);
345
342
  if (!doc || !collection)
346
343
  return ref.id;
347
- // EC-296/EC-242: a function `titleField` (title pattern) composes the title
348
- // from the document's resolved values (active locale). Only resolve the full
349
- // value set when a function is declared — the common string `titleField` case
350
- // still resolves the single title field below (primaryField === the title field).
351
- if (typeof collection.titleField === 'function') {
352
- const values = {};
353
- for (const field of collection.fields) {
354
- values[field.name] = resolveField(collection, doc, field.name, activeLocale, localeConfig).value;
355
- }
356
- return resolveDocumentTitle(collection, values, ref.id);
357
- }
358
- // Resolve through the locale layer in the ambient active locale (EC-176)
359
- // with EC-018 fallback, so localized titles follow the switcher.
360
- const { value } = resolveField(collection, doc, primaryField(collection), activeLocale, localeConfig);
361
- return typeof value === 'string' && value ? value : ref.id;
344
+ // EC-296/EC-242/EC-580: THE identity resolver (a string `titleField` or a
345
+ // function pattern) over the document's displayed view in the ambient active
346
+ // locale (EC-176) — `valuesInLocale`, with the EC-018 fallback — so localized
347
+ // titles follow the switcher and agree with the list title column and the
348
+ // detail header, which read the same view.
349
+ return resolveDocumentTitle(collection, valuesInLocale(collection, doc, activeLocale, localeConfig), ref.id);
362
350
  }, [documents, registry, localeConfig, activeLocale]);
363
351
  const saveDocument = useCallback((doc) => {
364
352
  // Replace-or-append (upsert): a brand-new document must still land even
@@ -380,7 +368,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
380
368
  // validates with exactly what the editor and the publish gate use and
381
369
  // filters the result to the committed cell — it is not a weaker check.
382
370
  isPublished,
383
- ...(assetExists ? { assetExists } : {}),
371
+ ...(findAsset ? { findAsset } : {}),
384
372
  // EC-445: the optimistic apply bumps the $updatedAt column (a rollback
385
373
  // leaves it — it self-corrects on the next hydration).
386
374
  applySession: (doc) => {
@@ -410,7 +398,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
410
398
  localeConfig,
411
399
  projectId,
412
400
  isPublished,
413
- assetExists,
401
+ findAsset,
414
402
  stampUpdatedAt,
415
403
  adoptSavedRecord,
416
404
  ]);
@@ -423,12 +411,12 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
423
411
  // concurrent invocation — StrictMode's double-invoked effect — can never
424
412
  // materialize a second singleton document).
425
413
  const insertNewDocument = useCallback((collectionId, options) => {
426
- const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current);
414
+ const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current, localeConfig);
427
415
  setDocuments((prev) => options?.guardExisting && hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]);
428
416
  stampUpdatedAt(doc); // EC-445: a fresh draft's $updatedAt starts at creation time
429
417
  persisterRef.current?.upsertDocument(doc);
430
418
  return doc;
431
- }, [registry, stampUpdatedAt]);
419
+ }, [registry, stampUpdatedAt, localeConfig]);
432
420
  const createDocument = useCallback((collectionId) => {
433
421
  const doc = insertNewDocument(collectionId);
434
422
  return { collection: collectionId, id: doc.id };
@@ -590,8 +578,8 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
590
578
  store,
591
579
  localeConfig,
592
580
  isPublished,
593
- ...(assetExists ? { assetExists } : {}),
594
- }), [store, localeConfig, isPublished, assetExists]);
581
+ ...(findAsset ? { findAsset } : {}),
582
+ }), [store, localeConfig, isPublished, findAsset]);
595
583
  const validate = useCallback((doc) => {
596
584
  const issues = validateDocument(registry, doc, validateOptions);
597
585
  // EC-492: a document whose composed URL no request can reach — a top-level
@@ -657,7 +645,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
657
645
  store: batchStore,
658
646
  localeConfig,
659
647
  isPublished,
660
- ...(assetExists ? { assetExists } : {}),
648
+ ...(findAsset ? { findAsset } : {}),
661
649
  });
662
650
  for (const doc of parsedBatch) {
663
651
  const issues = validateDocument(registry, doc, importOptions);
@@ -681,7 +669,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
681
669
  report.added = accepted.length;
682
670
  }
683
671
  return report;
684
- }, [registry, documents, localeConfig, isPublished, assetExists]);
672
+ }, [registry, documents, localeConfig, isPublished, findAsset]);
685
673
  const isFieldReadOnly = useCallback((field) => field.form?.readOnly === true, []);
686
674
  // --- Views (EC-246b: config-as-code) --------------------------------------
687
675
  // EC-445: the reserved pseudo-columns a CMS collection's list surfaces. Every
@@ -770,7 +758,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
770
758
  }
771
759
  // The next document for a set-field item: fold the value into the active
772
760
  // locale's bag (localized) or the shared `values` (non-localized).
773
- const setFieldDoc = (doc) => applyCellValue(collection, doc, setField.field, activeLocale, setField.value);
761
+ const setFieldDoc = (doc) => writeField(collection, doc, setField.field, activeLocale, setField.value);
774
762
  let dispatch;
775
763
  const persister = persisterRef.current;
776
764
  if (persister && projectId !== undefined) {
@@ -823,13 +811,16 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
823
811
  store: createDocumentStore(documentsRef.current),
824
812
  localeConfig,
825
813
  isPublished,
826
- ...(assetExists ? { assetExists } : {}),
814
+ ...(findAsset ? { findAsset } : {}),
827
815
  })),
828
816
  // EC-425: bulk publish joins the single-document clean point — the
829
817
  // SAME shared hidden-strip `doPublish` runs (EC-273 field values +
830
818
  // EC-394 component props, session registry), per item.
819
+ // EC-580: in the session's locale views — the single publish's same step.
831
820
  ...(collection
832
- ? { cleanForPublish: (doc) => cleanHiddenForPublish(collection, doc) }
821
+ ? {
822
+ cleanForPublish: (doc) => cleanHiddenForPublish(collection, doc, undefined, localeConfig),
823
+ }
833
824
  : {}),
834
825
  // Land a cleaned draft exactly the way the editor's publish does:
835
826
  // session apply + the persister's self-typing upsert (EC-277 manifest
@@ -870,7 +861,7 @@ function CmsSession({ children, projectId, initialCollections, initialDocuments,
870
861
  activeLocale,
871
862
  setPublished,
872
863
  isPublished,
873
- assetExists,
864
+ findAsset,
874
865
  adoptSavedRecord,
875
866
  stampHasUnpublishedChanges,
876
867
  ]);