@elytracms/studio 0.0.67 → 0.0.69

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 (89) hide show
  1. package/dist/components/assets/asset-detail.d.ts +4 -2
  2. package/dist/components/assets/asset-detail.js +4 -2
  3. package/dist/components/assets/asset-detail.js.map +1 -1
  4. package/dist/components/assets/assets-collection-view-source.d.ts +30 -19
  5. package/dist/components/assets/assets-collection-view-source.js +55 -23
  6. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  7. package/dist/components/builder/canvas-chrome-css.js +6 -1
  8. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  9. package/dist/components/builder/property-inspector.d.ts +0 -21
  10. package/dist/components/builder/property-inspector.js +5 -22
  11. package/dist/components/builder/property-inspector.js.map +1 -1
  12. package/dist/components/builder/selection-overlay.d.ts +17 -4
  13. package/dist/components/builder/selection-overlay.js +17 -15
  14. package/dist/components/builder/selection-overlay.js.map +1 -1
  15. package/dist/components/builder/spacing-handles.d.ts +26 -1
  16. package/dist/components/builder/spacing-handles.js +66 -3
  17. package/dist/components/builder/spacing-handles.js.map +1 -1
  18. package/dist/components/cms/asset-picker-core.d.ts +52 -0
  19. package/dist/components/cms/asset-picker-core.js +69 -0
  20. package/dist/components/cms/asset-picker-core.js.map +1 -0
  21. package/dist/components/cms/asset-picker.d.ts +16 -4
  22. package/dist/components/cms/asset-picker.js +34 -50
  23. package/dist/components/cms/asset-picker.js.map +1 -1
  24. package/dist/components/cms/bulk-bar.d.ts +11 -1
  25. package/dist/components/cms/bulk-bar.js +10 -3
  26. package/dist/components/cms/bulk-bar.js.map +1 -1
  27. package/dist/components/cms/collection-table.js +19 -2
  28. package/dist/components/cms/collection-table.js.map +1 -1
  29. package/dist/components/cms/detail-editor.js +45 -54
  30. package/dist/components/cms/detail-editor.js.map +1 -1
  31. package/dist/components/cms/field-control.js +5 -1
  32. package/dist/components/cms/field-control.js.map +1 -1
  33. package/dist/components/cms/prop-fields.js +5 -1
  34. package/dist/components/cms/prop-fields.js.map +1 -1
  35. package/dist/components/cms/prop-visibility.d.ts +3 -0
  36. package/dist/components/cms/prop-visibility.js +31 -0
  37. package/dist/components/cms/prop-visibility.js.map +1 -0
  38. package/dist/components/cms/reference-confirm.js +5 -1
  39. package/dist/components/cms/reference-confirm.js.map +1 -1
  40. package/dist/components/cms/rich-text/component-embed.d.ts +8 -6
  41. package/dist/components/cms/rich-text/component-embed.js +21 -62
  42. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  43. package/dist/components/cms/rich-text/editor.d.ts +33 -4
  44. package/dist/components/cms/rich-text/editor.js +52 -22
  45. package/dist/components/cms/rich-text/editor.js.map +1 -1
  46. package/dist/components/cms/rich-text/embed-props.d.ts +60 -27
  47. package/dist/components/cms/rich-text/embed-props.js +102 -45
  48. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  49. package/dist/components/rail-view-source.d.ts +50 -1
  50. package/dist/components/rail-view-source.js +56 -2
  51. package/dist/components/rail-view-source.js.map +1 -1
  52. package/dist/components/redirects/redirects-collection-view-source.d.ts +3 -2
  53. package/dist/components/redirects/redirects-collection-view-source.js +4 -3
  54. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  55. package/dist/components/users/users-collection-view-source.js +1 -1
  56. package/dist/components/users/users-collection-view-source.js.map +1 -1
  57. package/dist/lib/collection-view/collection-view-source.d.ts +12 -0
  58. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  59. package/dist/lib/dnd/gap-drag.d.ts +8 -1
  60. package/dist/lib/dnd/gap-drag.js +4 -0
  61. package/dist/lib/dnd/gap-drag.js.map +1 -1
  62. package/dist/lib/i18n/messages.de.js +3 -2
  63. package/dist/lib/i18n/messages.de.js.map +1 -1
  64. package/dist/lib/i18n/messages.en.d.ts +1 -1
  65. package/dist/lib/i18n/messages.en.js +4 -2
  66. package/dist/lib/i18n/messages.en.js.map +1 -1
  67. package/dist/lib/references/references-store.d.ts +8 -1
  68. package/dist/lib/references/references-store.js +12 -2
  69. package/dist/lib/references/references-store.js.map +1 -1
  70. package/dist/ui/expandable-surface.d.ts +35 -25
  71. package/dist/ui/expandable-surface.js +41 -21
  72. package/dist/ui/expandable-surface.js.map +1 -1
  73. package/dist/ui/index.d.ts +1 -0
  74. package/dist/ui/index.js +1 -0
  75. package/dist/ui/index.js.map +1 -1
  76. package/dist/ui/surface-geometry.d.ts +71 -0
  77. package/dist/ui/surface-geometry.js +64 -0
  78. package/dist/ui/surface-geometry.js.map +1 -0
  79. package/package.json +2 -2
  80. package/src/styles/studio.css +30 -0
  81. package/dist/components/cms/rich-text/embed-inspector-context.d.ts +0 -50
  82. package/dist/components/cms/rich-text/embed-inspector-context.js +0 -12
  83. package/dist/components/cms/rich-text/embed-inspector-context.js.map +0 -1
  84. package/dist/components/cms/rich-text/embed-inspector.d.ts +0 -25
  85. package/dist/components/cms/rich-text/embed-inspector.js +0 -48
  86. package/dist/components/cms/rich-text/embed-inspector.js.map +0 -1
  87. package/dist/components/cms/use-embed-inspector-host.d.ts +0 -12
  88. package/dist/components/cms/use-embed-inspector-host.js +0 -17
  89. package/dist/components/cms/use-embed-inspector-host.js.map +0 -1
@@ -1,29 +1,39 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Lock } from 'lucide-react';
3
3
  import { isStaticProp, staticProp } from '@elytracms/core/project-graph';
4
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Field, Stack, Text, } from '@elytracms/studio/ui';
4
+ import { Badge, ExpandableSurface, Field, Row, Separator, Stack, Text, } from '@elytracms/studio/ui';
5
5
  import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
6
6
  import { humanizeName } from '@elytracms/studio/lib/utils';
7
7
  import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
8
8
  import { PropValueControl } from '../prop-fields.js';
9
+ import { visiblePropFields } from '../prop-visibility.js';
9
10
  /**
10
- * Prop editing for rich-text component embeds (EC-314 phase 4).
11
+ * Prop editing for rich-text component embeds (EC-314 phase 4; stage surface
12
+ * EC-430/AD-D).
11
13
  *
12
14
  * An embedded component is a full `ComponentNode` stored verbatim at
13
- * `componentEmbed.attrs.node`. This dialog edits that node's *props* through the
14
- * SAME EC-190 "props-as-fields" control stack the canvas inspector and `blocks`
15
- * field use ({@link PropValueControl}) — asset props open the library picker,
16
- * relation props the document picker, etc. — never a parallel form system.
15
+ * `componentEmbed.attrs.node`. The {@link EmbedPropsStage} edits that node's *props*
16
+ * through the SAME EC-190 "props-as-fields" control stack the canvas inspector and
17
+ * `blocks` field use ({@link PropValueControl}) — asset props open the library
18
+ * picker, relation props the document picker, etc. — never a parallel form system.
17
19
  *
18
- * Fidelity contract: a working `draft` is seeded from the stored node when the
19
- * dialog opens; the dialog only writes back on Apply, and only when the draft is
20
- * *structurally different* ({@link componentNodesEqual}) — so opening and closing
21
- * without a real change never dirties the field. The upstream write goes through a
22
- * single ProseMirror `updateAttributes` transaction (undoable), never a storage
23
- * rewrite; the embedded node's `id`/`componentId`/`slots` pass through untouched.
20
+ * One surface, reachable everywhere (ratified AD-D, 2026-07-16): "Edit settings" on
21
+ * an embed card opens the centered stage (`ExpandableSurface variant="stage"
22
+ * preset="form"`) identically from the CANVAS tab and the form view. It replaced
23
+ * BOTH halves of the 0.0.41 fork — the selection-driven sidebar swap (which only
24
+ * rendered in the form view, leaving the canvas tab's "Edit settings" dead) and the
25
+ * modal fallback.
26
+ *
27
+ * Value contract (unchanged from the 0.0.41 sidebar inspector): write-back is
28
+ * **immediate per prop change** — each edit commits through `onChange` (one
29
+ * ProseMirror `updateAttributes` transaction upstream, undoable), guarded by
30
+ * {@link resolveEmbedWrite} so an unchanged write never dirties the field. There is
31
+ * no draft+Apply and no save/cancel of its own: the stage is the same value flow
32
+ * relocated, lightly dismissible (Escape / backdrop / header close) per the
33
+ * forms-never-modal spirit. The dialog fork's draft+Apply semantics died with it.
24
34
  *
25
35
  * `blocks`-typed props (nested composition) are NOT edited here — that needs the
26
- * canvas, not a popover. They are excluded from the form and surfaced as a note.
36
+ * canvas, not a form. They are excluded from the form and surfaced as a note.
27
37
  */
28
38
  /** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */
29
39
  export function setComponentNodeProp(node, name, value) {
@@ -40,10 +50,10 @@ export function componentNodesEqual(a, b) {
40
50
  return stableStringify(a) === stableStringify(b);
41
51
  }
42
52
  /**
43
- * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector):
44
- * the node to commit for an edit, or `undefined` when it is a structural no-op so an
45
- * unchanged write never dirties the field. Same contract the old draft+Apply dialog
46
- * enforced ({@link componentNodesEqual}), applied per keystroke instead of on Apply.
53
+ * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector,
54
+ * inherited verbatim by the EC-430 stage form): the node to commit for an edit, or
55
+ * `undefined` when it is a structural no-op so an unchanged write never dirties the
56
+ * field ({@link componentNodesEqual}), applied per keystroke.
47
57
  */
48
58
  export function resolveEmbedWrite(current, next) {
49
59
  return componentNodesEqual(next, current) ? undefined : next;
@@ -59,10 +69,6 @@ function stableStringify(value) {
59
69
  .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
60
70
  return `{${entries.join(',')}}`;
61
71
  }
62
- /** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */
63
- export function editableEmbedProps(manifest) {
64
- return Object.entries(manifest.props).filter(([, spec]) => spec.type !== 'blocks');
65
- }
66
72
  /** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */
67
73
  export function blocksEmbedProps(manifest) {
68
74
  return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks');
@@ -70,7 +76,7 @@ export function blocksEmbedProps(manifest) {
70
76
  /**
71
77
  * A lean one-line summary of an embed's static scalar props (`Label: value`,
72
78
  * up to three), for the card. Skips unset props and structured values (refs /
73
- * objects / arrays) — those are edited in the dialog, not summarised.
79
+ * objects / arrays) — those are edited in the stage form, not summarised.
74
80
  */
75
81
  export function embedPropsSummary(node, manifest) {
76
82
  const parts = [];
@@ -88,28 +94,79 @@ export function embedPropsSummary(node, manifest) {
88
94
  }
89
95
  return parts.length > 0 ? parts.join(' · ') : undefined;
90
96
  }
91
- /** The edit-props dialog opened from an embed card. */
92
- export function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }) {
93
- const [draft, setDraft] = useState(node);
94
- // Re-seed the working draft from the stored node whenever the dialog opens, so a
95
- // cancelled edit is discarded and a reopen starts from the committed value.
96
- useEffect(() => {
97
- if (open)
98
- setDraft(node);
99
- }, [open, node]);
100
- const editable = editableEmbedProps(manifest);
97
+ /**
98
+ * The embed prop form body — pure of surface chrome so it smoke-renders in the node
99
+ * test env (the stage itself portals and renders `null` without a DOM).
100
+ *
101
+ * EC-393 parity: rows filter through the SAME {@link visiblePropFields} core the
102
+ * canvas property inspector uses — a predicate-hidden row (`form.hidden` over the
103
+ * sibling prop values) unmounts/remounts live as its driver prop flips, because the
104
+ * committed node re-renders this form. EC-395 parity: `form.readOnly` renders the
105
+ * document form's Lock badge next to the label and disables the delegated control
106
+ * (`PropValueControl` reads the same flag); `form.control` and `form.description`
107
+ * route through the same delegation.
108
+ */
109
+ export function EmbedStageForm({ node, manifest, onChange }) {
110
+ const visible = visiblePropFields(manifest, node);
101
111
  const blocks = blocksEmbedProps(manifest);
102
- function apply() {
103
- // No structural change ⇒ never write (opening + closing must not dirty the field).
104
- if (!componentNodesEqual(draft, node))
105
- onSave(draft);
106
- onOpenChange(false);
112
+ function set(name, value) {
113
+ const next = resolveEmbedWrite(node, setComponentNodeProp(node, name, value));
114
+ if (next)
115
+ onChange(next);
116
+ }
117
+ function clear(name) {
118
+ const next = resolveEmbedWrite(node, clearComponentNodeProp(node, name));
119
+ if (next)
120
+ onChange(next);
107
121
  }
108
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: translate('richText.embed.editTitle', {
109
- name: componentDisplayName(node.componentId, manifest),
110
- }) }) }), _jsxs(Stack, { gap: "4", className: "max-h-[60vh] overflow-y-auto", children: [editable.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('richText.embed.noProps') })) : (editable.map(([name, spec]) => {
111
- const id = `embed-${node.id}-${name}`;
112
- return (_jsx(Field, { label: localizedLabel(spec.form?.label, name), htmlFor: id, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: draft.props?.[name], onSet: (v) => setDraft((d) => setComponentNodeProp(d, name, v)), onClear: () => setDraft((d) => clearComponentNodeProp(d, name)) }) }, name));
113
- })), blocks.length > 0 ? (_jsx(Text, { size: "xs", tone: "subtle", children: translate('richText.embed.blocksNote') })) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: translate('common.cancel') }), _jsx(Button, { type: "button", onClick: apply, children: translate('common.apply') })] })] }) }));
122
+ return (_jsxs(Stack, { gap: "4", children: [visible.length === 0 ? (_jsx(Text, { size: "sm", tone: "muted", children: translate('richText.embed.noProps') })) : (visible.map(([name, spec]) => {
123
+ const id = `embed-${node.id}-${name}`;
124
+ return (_jsx(Field, { label: spec.form?.readOnly === true ? (_jsxs(Row, { gap: "2", children: [_jsx("span", { children: localizedLabel(spec.form?.label, name) }), _jsxs(Badge, { variant: "outline", children: [_jsx(Lock, { className: "size-3" }), " ", translate('cms.editor.readOnlyBadge')] })] })) : (localizedLabel(spec.form?.label, name)), htmlFor: id, hint: localizedLabel(spec.form?.description) || undefined, children: _jsx(PropValueControl, { id: id, name: name, spec: spec, value: node.props?.[name], onSet: (v) => set(name, v), onClear: () => clear(name) }) }, name));
125
+ })), blocks.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx(Text, { size: "xs", tone: "subtle", children: translate('richText.embed.blocksNote') })] })) : null] }));
126
+ }
127
+ /**
128
+ * The stage-body focus handoff (a module-level constant so React never re-invokes it
129
+ * on re-renders — a per-keystroke re-invocation would churn the guard). The stage
130
+ * opens from a card INSIDE a focused TipTap editor, and prosemirror-view
131
+ * preventDefaults every Escape while its contenteditable holds focus
132
+ * (`capturekeys.ts` — its "dangerous default" backdrop map), which the surface's
133
+ * EC-392 bridge reads as "handled by a layer above" — so Escape would never dismiss,
134
+ * and stray typing would land in the covered document. Moving focus into the stage
135
+ * on mount hands Escape to the layer stack; the `focusin` guard keeps it there,
136
+ * because ProseMirror re-asserts focus on its editable off deferred click handling
137
+ * and window-refocus events. The guard is NOT a focus trap: it only reclaims from a
138
+ * ProseMirror editable OUTSIDE the stage (unreachable to the user behind the
139
+ * backdrop — only programmatic steals hit this), never from the stage's own controls
140
+ * (incl. nested rich-text prop editors, `el.contains`) or a picker dialog above.
141
+ * Per-caller policy, deliberately NOT in ExpandableSurface: the rich-text prose
142
+ * stage RELOCATES the editor, so there the editor keeping focus is the point.
143
+ * React 19 ref-cleanup unregisters the guard when the stage unmounts.
144
+ */
145
+ const focusStageBody = (el) => {
146
+ if (!el)
147
+ return undefined;
148
+ el.focus();
149
+ const guard = () => {
150
+ const active = document.activeElement;
151
+ if (el.contains(active))
152
+ return;
153
+ if (active instanceof HTMLElement && active.closest('.ProseMirror'))
154
+ el.focus();
155
+ };
156
+ document.addEventListener('focusin', guard);
157
+ return () => document.removeEventListener('focusin', guard);
158
+ };
159
+ /**
160
+ * The embed prop inspector on the centered stage (EC-430/AD-D) — mounted by the
161
+ * embed card's "Edit settings" from ANY rich-text surface (form view, canvas-tab
162
+ * prop inspector, or inside the expanded rich-text stage — a later mount rides the
163
+ * EC-392 layer stack above it, and pickers opened from these controls stack above
164
+ * this stage in turn). Hook-free by construction so the geometry contract is
165
+ * pinnable by direct invocation in the node test env.
166
+ */
167
+ export function EmbedPropsStage({ node, manifest, onChange, onDismiss }) {
168
+ return (_jsx(ExpandableSurface, { variant: "stage", preset: "form", title: translate('richText.embed.editTitle', {
169
+ name: componentDisplayName(node.componentId, manifest),
170
+ }), closeLabel: translate('common.close'), onDismiss: onDismiss, children: _jsx("div", { ref: focusStageBody, tabIndex: -1, className: "min-h-0 flex-1 overflow-y-auto p-6 outline-none", children: _jsx(EmbedStageForm, { node: node, manifest: manifest, onChange: onChange }) }) }));
114
171
  }
115
172
  //# sourceMappingURL=embed-props.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"embed-props.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-props.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react'\nimport { isStaticProp, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { PropValueControl } from '../prop-fields'\n\n/**\n * Prop editing for rich-text component embeds (EC-314 phase 4).\n *\n * An embedded component is a full `ComponentNode` stored verbatim at\n * `componentEmbed.attrs.node`. This dialog edits that node's *props* through the\n * SAME EC-190 \"props-as-fields\" control stack the canvas inspector and `blocks`\n * field use ({@link PropValueControl}) — asset props open the library picker,\n * relation props the document picker, etc. — never a parallel form system.\n *\n * Fidelity contract: a working `draft` is seeded from the stored node when the\n * dialog opens; the dialog only writes back on Apply, and only when the draft is\n * *structurally different* ({@link componentNodesEqual}) — so opening and closing\n * without a real change never dirties the field. The upstream write goes through a\n * single ProseMirror `updateAttributes` transaction (undoable), never a storage\n * rewrite; the embedded node's `id`/`componentId`/`slots` pass through untouched.\n *\n * `blocks`-typed props (nested composition) are NOT edited here — that needs the\n * canvas, not a popover. They are excluded from the form and surfaced as a note.\n */\n\n/** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */\nexport function setComponentNodeProp(\n node: ComponentNode,\n name: string,\n value: JsonValue,\n): ComponentNode {\n return { ...node, props: { ...node.props, [name]: staticProp(value) } }\n}\n\n/** Remove a prop (revert to the manifest default), preserving everything else. */\nexport function clearComponentNodeProp(node: ComponentNode, name: string): ComponentNode {\n const props = { ...node.props }\n delete props[name]\n return { ...node, props }\n}\n\n/** Key-order-insensitive structural equality — a redundant \"save\" must not write. */\nexport function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean {\n return stableStringify(a) === stableStringify(b)\n}\n\n/**\n * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector):\n * the node to commit for an edit, or `undefined` when it is a structural no-op so an\n * unchanged write never dirties the field. Same contract the old draft+Apply dialog\n * enforced ({@link componentNodesEqual}), applied per keystroke instead of on Apply.\n */\nexport function resolveEmbedWrite(current: ComponentNode, next: ComponentNode): ComponentNode | undefined {\n return componentNodesEqual(next, current) ? undefined : next\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n return `{${entries.join(',')}}`\n}\n\n/** The manifest props editable inline: every non-`blocks` prop (composition needs the canvas). */\nexport function editableEmbedProps(manifest: ComponentManifest): Array<[string, PropField]> {\n return Object.entries(manifest.props).filter(([, spec]) => spec.type !== 'blocks')\n}\n\n/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */\nexport function blocksEmbedProps(manifest: ComponentManifest): Array<[string, PropField]> {\n return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks')\n}\n\n/**\n * A lean one-line summary of an embed's static scalar props (`Label: value`,\n * up to three), for the card. Skips unset props and structured values (refs /\n * objects / arrays) — those are edited in the dialog, not summarised.\n */\nexport function embedPropsSummary(\n node: ComponentNode,\n manifest?: ComponentManifest,\n): string | undefined {\n const parts: string[] = []\n for (const [name, value] of Object.entries(node.props ?? {})) {\n if (!isStaticProp(value)) continue\n const raw = value.value\n if (raw === null || raw === undefined || typeof raw === 'object') continue\n const label = manifest?.props[name]?.form?.label\n const labelText = label ? localizedLabel(label, name) : humanizeName(name)\n parts.push(`${labelText}: ${String(raw)}`)\n if (parts.length >= 3) break\n }\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\nexport interface EmbedPropsDialogProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n /** The embedded component node whose props are being edited. */\n node: ComponentNode\n /** Its manifest — drives which prop fields render. */\n manifest: ComponentManifest\n /** Commit the edited node (upstream writes it through one ProseMirror transaction). */\n onSave: (node: ComponentNode) => void\n}\n\n/** The edit-props dialog opened from an embed card. */\nexport function EmbedPropsDialog({ open, onOpenChange, node, manifest, onSave }: EmbedPropsDialogProps) {\n const [draft, setDraft] = useState<ComponentNode>(node)\n\n // Re-seed the working draft from the stored node whenever the dialog opens, so a\n // cancelled edit is discarded and a reopen starts from the committed value.\n useEffect(() => {\n if (open) setDraft(node)\n }, [open, node])\n\n const editable = editableEmbedProps(manifest)\n const blocks = blocksEmbedProps(manifest)\n\n function apply() {\n // No structural change ⇒ never write (opening + closing must not dirty the field).\n if (!componentNodesEqual(draft, node)) onSave(draft)\n onOpenChange(false)\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate('richText.embed.editTitle', {\n name: componentDisplayName(node.componentId, manifest),\n })}\n </DialogTitle>\n </DialogHeader>\n <Stack gap=\"4\" className=\"max-h-[60vh] overflow-y-auto\">\n {editable.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.embed.noProps')}\n </Text>\n ) : (\n editable.map(([name, spec]) => {\n const id = `embed-${node.id}-${name}`\n return (\n <Field key={name} label={localizedLabel(spec.form?.label, name)} htmlFor={id}>\n <PropValueControl\n id={id}\n name={name}\n spec={spec}\n value={draft.props?.[name]}\n onSet={(v) => setDraft((d) => setComponentNodeProp(d, name, v))}\n onClear={() => setDraft((d) => clearComponentNodeProp(d, name))}\n />\n </Field>\n )\n })\n )}\n {blocks.length > 0 ? (\n <Text size=\"xs\" tone=\"subtle\">\n {translate('richText.embed.blocksNote')}\n </Text>\n ) : null}\n </Stack>\n <DialogFooter>\n <Button type=\"button\" variant=\"ghost\" onClick={() => onOpenChange(false)}>\n {translate('common.cancel')}\n </Button>\n <Button type=\"button\" onClick={apply}>\n {translate('common.apply')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
1
+ {"version":3,"file":"embed-props.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-props.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Lock } from 'lucide-react'\nimport { isStaticProp, staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport type { ComponentManifest, PropField } from '@elytracms/core/component-registry'\nimport {\n Badge,\n ExpandableSurface,\n Field,\n Row,\n Separator,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { humanizeName } from '@elytracms/studio/lib/utils'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { PropValueControl } from '../prop-fields'\nimport { visiblePropFields } from '../prop-visibility'\n\n/**\n * Prop editing for rich-text component embeds (EC-314 phase 4; stage surface\n * EC-430/AD-D).\n *\n * An embedded component is a full `ComponentNode` stored verbatim at\n * `componentEmbed.attrs.node`. The {@link EmbedPropsStage} edits that node's *props*\n * through the SAME EC-190 \"props-as-fields\" control stack the canvas inspector and\n * `blocks` field use ({@link PropValueControl}) — asset props open the library\n * picker, relation props the document picker, etc. — never a parallel form system.\n *\n * One surface, reachable everywhere (ratified AD-D, 2026-07-16): \"Edit settings\" on\n * an embed card opens the centered stage (`ExpandableSurface variant=\"stage\"\n * preset=\"form\"`) identically from the CANVAS tab and the form view. It replaced\n * BOTH halves of the 0.0.41 fork — the selection-driven sidebar swap (which only\n * rendered in the form view, leaving the canvas tab's \"Edit settings\" dead) and the\n * modal fallback.\n *\n * Value contract (unchanged from the 0.0.41 sidebar inspector): write-back is\n * **immediate per prop change** — each edit commits through `onChange` (one\n * ProseMirror `updateAttributes` transaction upstream, undoable), guarded by\n * {@link resolveEmbedWrite} so an unchanged write never dirties the field. There is\n * no draft+Apply and no save/cancel of its own: the stage is the same value flow\n * relocated, lightly dismissible (Escape / backdrop / header close) per the\n * forms-never-modal spirit. The dialog fork's draft+Apply semantics died with it.\n *\n * `blocks`-typed props (nested composition) are NOT edited here — that needs the\n * canvas, not a form. They are excluded from the form and surfaced as a note.\n */\n\n/** Set a prop to a fresh static value, preserving identity, slots, and sibling props. */\nexport function setComponentNodeProp(\n node: ComponentNode,\n name: string,\n value: JsonValue,\n): ComponentNode {\n return { ...node, props: { ...node.props, [name]: staticProp(value) } }\n}\n\n/** Remove a prop (revert to the manifest default), preserving everything else. */\nexport function clearComponentNodeProp(node: ComponentNode, name: string): ComponentNode {\n const props = { ...node.props }\n delete props[name]\n return { ...node, props }\n}\n\n/** Key-order-insensitive structural equality — a redundant \"save\" must not write. */\nexport function componentNodesEqual(a: ComponentNode, b: ComponentNode): boolean {\n return stableStringify(a) === stableStringify(b)\n}\n\n/**\n * The no-dirty guard for immediate per-prop write-back (0.0.41 sidebar inspector,\n * inherited verbatim by the EC-430 stage form): the node to commit for an edit, or\n * `undefined` when it is a structural no-op so an unchanged write never dirties the\n * field ({@link componentNodesEqual}), applied per keystroke.\n */\nexport function resolveEmbedWrite(current: ComponentNode, next: ComponentNode): ComponentNode | undefined {\n return componentNodesEqual(next, current) ? undefined : next\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n return `{${entries.join(',')}}`\n}\n\n/** The manifest's `blocks` (composition) props — excluded from the form, shown as a note. */\nexport function blocksEmbedProps(manifest: ComponentManifest): Array<[string, PropField]> {\n return Object.entries(manifest.props).filter(([, spec]) => spec.type === 'blocks')\n}\n\n/**\n * A lean one-line summary of an embed's static scalar props (`Label: value`,\n * up to three), for the card. Skips unset props and structured values (refs /\n * objects / arrays) — those are edited in the stage form, not summarised.\n */\nexport function embedPropsSummary(\n node: ComponentNode,\n manifest?: ComponentManifest,\n): string | undefined {\n const parts: string[] = []\n for (const [name, value] of Object.entries(node.props ?? {})) {\n if (!isStaticProp(value)) continue\n const raw = value.value\n if (raw === null || raw === undefined || typeof raw === 'object') continue\n const label = manifest?.props[name]?.form?.label\n const labelText = label ? localizedLabel(label, name) : humanizeName(name)\n parts.push(`${labelText}: ${String(raw)}`)\n if (parts.length >= 3) break\n }\n return parts.length > 0 ? parts.join(' · ') : undefined\n}\n\nexport interface EmbedStageFormProps {\n /** The embedded component node whose props are being edited (the STORED node). */\n node: ComponentNode\n /** Its manifest — drives which prop fields render. */\n manifest: ComponentManifest\n /**\n * Commit an edited node. Upstream writes it through one ProseMirror\n * `updateAttributes` transaction (undoable); the no-dirty guard has already been\n * applied here, so this only fires on a real structural change.\n */\n onChange: (node: ComponentNode) => void\n}\n\n/**\n * The embed prop form body — pure of surface chrome so it smoke-renders in the node\n * test env (the stage itself portals and renders `null` without a DOM).\n *\n * EC-393 parity: rows filter through the SAME {@link visiblePropFields} core the\n * canvas property inspector uses — a predicate-hidden row (`form.hidden` over the\n * sibling prop values) unmounts/remounts live as its driver prop flips, because the\n * committed node re-renders this form. EC-395 parity: `form.readOnly` renders the\n * document form's Lock badge next to the label and disables the delegated control\n * (`PropValueControl` reads the same flag); `form.control` and `form.description`\n * route through the same delegation.\n */\nexport function EmbedStageForm({ node, manifest, onChange }: EmbedStageFormProps) {\n const visible = visiblePropFields(manifest, node)\n const blocks = blocksEmbedProps(manifest)\n\n function set(name: string, value: JsonValue) {\n const next = resolveEmbedWrite(node, setComponentNodeProp(node, name, value))\n if (next) onChange(next)\n }\n function clear(name: string) {\n const next = resolveEmbedWrite(node, clearComponentNodeProp(node, name))\n if (next) onChange(next)\n }\n\n return (\n <Stack gap=\"4\">\n {visible.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.embed.noProps')}\n </Text>\n ) : (\n visible.map(([name, spec]) => {\n const id = `embed-${node.id}-${name}`\n return (\n <Field\n key={name}\n label={\n spec.form?.readOnly === true ? (\n // EC-395: the SAME read-only badge the document form and the canvas\n // prop inspector render next to a locked label — identical icon,\n // i18n key, and `Badge` variant, never a bespoke treatment.\n <Row gap=\"2\">\n <span>{localizedLabel(spec.form?.label, name)}</span>\n <Badge variant=\"outline\">\n <Lock className=\"size-3\" /> {translate('cms.editor.readOnlyBadge')}\n </Badge>\n </Row>\n ) : (\n localizedLabel(spec.form?.label, name)\n )\n }\n htmlFor={id}\n hint={localizedLabel(spec.form?.description) || undefined}\n >\n <PropValueControl\n id={id}\n name={name}\n spec={spec}\n value={node.props?.[name]}\n onSet={(v) => set(name, v)}\n onClear={() => clear(name)}\n />\n </Field>\n )\n })\n )}\n {blocks.length > 0 ? (\n <>\n <Separator />\n <Text size=\"xs\" tone=\"subtle\">\n {translate('richText.embed.blocksNote')}\n </Text>\n </>\n ) : null}\n </Stack>\n )\n}\n\nexport interface EmbedPropsStageProps extends EmbedStageFormProps {\n /** Dismiss request (Escape / backdrop / header close). The caller owns the open state. */\n onDismiss: () => void\n}\n\n/**\n * The stage-body focus handoff (a module-level constant so React never re-invokes it\n * on re-renders — a per-keystroke re-invocation would churn the guard). The stage\n * opens from a card INSIDE a focused TipTap editor, and prosemirror-view\n * preventDefaults every Escape while its contenteditable holds focus\n * (`capturekeys.ts` — its \"dangerous default\" backdrop map), which the surface's\n * EC-392 bridge reads as \"handled by a layer above\" — so Escape would never dismiss,\n * and stray typing would land in the covered document. Moving focus into the stage\n * on mount hands Escape to the layer stack; the `focusin` guard keeps it there,\n * because ProseMirror re-asserts focus on its editable off deferred click handling\n * and window-refocus events. The guard is NOT a focus trap: it only reclaims from a\n * ProseMirror editable OUTSIDE the stage (unreachable to the user behind the\n * backdrop — only programmatic steals hit this), never from the stage's own controls\n * (incl. nested rich-text prop editors, `el.contains`) or a picker dialog above.\n * Per-caller policy, deliberately NOT in ExpandableSurface: the rich-text prose\n * stage RELOCATES the editor, so there the editor keeping focus is the point.\n * React 19 ref-cleanup unregisters the guard when the stage unmounts.\n */\nconst focusStageBody = (el: HTMLDivElement | null) => {\n if (!el) return undefined\n el.focus()\n const guard = () => {\n const active = document.activeElement\n if (el.contains(active)) return\n if (active instanceof HTMLElement && active.closest('.ProseMirror')) el.focus()\n }\n document.addEventListener('focusin', guard)\n return () => document.removeEventListener('focusin', guard)\n}\n\n/**\n * The embed prop inspector on the centered stage (EC-430/AD-D) — mounted by the\n * embed card's \"Edit settings\" from ANY rich-text surface (form view, canvas-tab\n * prop inspector, or inside the expanded rich-text stage — a later mount rides the\n * EC-392 layer stack above it, and pickers opened from these controls stack above\n * this stage in turn). Hook-free by construction so the geometry contract is\n * pinnable by direct invocation in the node test env.\n */\nexport function EmbedPropsStage({ node, manifest, onChange, onDismiss }: EmbedPropsStageProps) {\n return (\n <ExpandableSurface\n variant=\"stage\"\n preset=\"form\"\n title={translate('richText.embed.editTitle', {\n name: componentDisplayName(node.componentId, manifest),\n })}\n closeLabel={translate('common.close')}\n onDismiss={onDismiss}\n >\n <div\n ref={focusStageBody}\n tabIndex={-1}\n className=\"min-h-0 flex-1 overflow-y-auto p-6 outline-none\"\n >\n <EmbedStageForm node={node} manifest={manifest} onChange={onChange} />\n </div>\n </ExpandableSurface>\n )\n}\n"]}
@@ -1,7 +1,9 @@
1
1
  import type { CmsDocument, CmsValidationIssue, CollectionDef, DocumentRef, FieldDef, LocaleConfig } from '@elytracms/core/cms-core';
2
- import type { OperationError } from '@elytracms/core/operations';
2
+ import type { OperationError, OperationResult } from '@elytracms/core/operations';
3
+ import type { ReferenceTarget } from '@elytracms/core/persistence';
3
4
  import type { TableViewState } from '@elytracms/studio/lib/cms-views';
4
5
  import type { CollectionViewCapabilities, CollectionViewSource, FieldAccess } from '@elytracms/studio/lib/collection-view/collection-view-source';
6
+ import type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions.js';
5
7
  /**
6
8
  * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks
7
9
  * (EC-327) — assets, users, redirects. All three hand-rolled identical
@@ -47,6 +49,32 @@ export declare function metaBagProjection(values: Record<string, unknown>, canon
47
49
  * `setError` — the factory's error-mapping plumbing, pulled out as a plain
48
50
  * function (no hook) so it is unit-testable without a `renderHook` harness. */
49
51
  export declare function createErrorMappedWriter<TArg, TResult>(write: (arg: TArg) => Promise<TResult>, normalize: (result: TResult) => OperationError | null, setError: (error: OperationError) => void): (arg: TArg) => void;
52
+ /** The store-facing input of {@link runRailBulkAction} — plain data/functions,
53
+ * so the rail bulk path is unit-testable without a hook harness. */
54
+ export interface RailBulkInput {
55
+ collectionId: string;
56
+ /** Current rail rows (lens documents) — the runner's lookup + validation input. */
57
+ documents: readonly CmsDocument[];
58
+ titleOf(ref: DocumentRef): string;
59
+ validate(doc: CmsDocument): CmsValidationIssue[];
60
+ /**
61
+ * Dispatch ONE already-confirmed delete (the EC-173 reference dialog ran
62
+ * before anything reaches this) and reconcile the rail store's result shape
63
+ * into an operation-shaped outcome — a failure here is a visible per-item
64
+ * failure in the report, never a swallowed one.
65
+ */
66
+ removeConfirmed(ref: DocumentRef): Promise<OperationResult<unknown>>;
67
+ }
68
+ /**
69
+ * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery
70
+ * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),
71
+ * wired to the rail's own store delete. Only `delete` is dispatchable — the
72
+ * rails have no publish lifecycle and no bulk set-field — and an unsupported
73
+ * action fails each item HONESTLY in the report rather than silently no-oping
74
+ * (the exact bug this replaces: the assets rail's bulk delete confirmed into a
75
+ * missing `source.bulk` and dropped the whole action on the floor).
76
+ */
77
+ export declare function runRailBulkAction(input: RailBulkInput, ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>;
50
78
  /** The fields shared by {@link RailAdapter} and the pure assembly input —
51
79
  * everything the factory copies straight onto the assembled source, or
52
80
  * defaults when a rail omits it. */
@@ -68,6 +96,12 @@ interface RailViewSourceCore {
68
96
  isFieldReadOnly?(field: FieldDef): boolean;
69
97
  /** Principal-based tri-state — only the users rail supplies one. */
70
98
  fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess;
99
+ /**
100
+ * How this rail's records are addressed in the reference index (EC-433) —
101
+ * only the assets rail supplies one (`{ type: 'asset' }`); absent ⇒ the
102
+ * confirm dialog's default document-shaped lookup.
103
+ */
104
+ referenceTarget?(ref: DocumentRef): ReferenceTarget;
71
105
  validate(doc: CmsDocument): CmsValidationIssue[];
72
106
  create(): DocumentRef;
73
107
  }
@@ -78,6 +112,14 @@ interface RailViewSourceCore {
78
112
  export interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {
79
113
  save(doc: CmsDocument): Promise<TSaveResult>;
80
114
  remove(ref: DocumentRef): Promise<TRemoveResult>;
115
+ /**
116
+ * The delete the BULK path dispatches (EC-433) — always post-confirmation
117
+ * (the EC-173 reference dialog ran first). Defaults to {@link remove}; the
118
+ * assets rail overrides it to pass `confirmInUse: true` so a confirmed bulk
119
+ * delete of a referenced asset actually deletes (mirroring the detail
120
+ * surface's confirmed delete) instead of re-blocking on the usage gate.
121
+ */
122
+ bulkRemove?(ref: DocumentRef): Promise<TRemoveResult>;
81
123
  normalizeSaveResult(result: TSaveResult): OperationError | null;
82
124
  normalizeRemoveResult(result: TRemoveResult): OperationError | null;
83
125
  }
@@ -90,6 +132,13 @@ interface RailViewSourceAssemblyInput extends RailViewSourceCore {
90
132
  setViewState: CollectionViewSource['setViewState'];
91
133
  save(doc: CmsDocument): void;
92
134
  remove(ref: DocumentRef): void;
135
+ /**
136
+ * The rail bulk runner (EC-433). Assembled onto the source ONLY when
137
+ * `capabilities.bulk` is set — the `CollectionViewSource` contract is
138
+ * "method present iff the flag is" — so a bulk-less rail's list never
139
+ * half-offers the affordance.
140
+ */
141
+ bulk?: CollectionViewSource['bulk'];
93
142
  }
94
143
  /**
95
144
  * Pure assembly of a {@link CollectionViewSource} from a rail's resolved
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
2
  import { defaultViewState } from '@elytracms/studio/lib/cms-views';
3
+ import { runBulkAction } from './cms/bulk-actions.js';
3
4
  /** The default normalizer for an {@link OkResult}: a failure maps to a
4
5
  * surfaced {@link OperationError}; a success maps to no error at all. */
5
6
  export function normalizeOkResult(result) {
@@ -34,6 +35,37 @@ export function createErrorMappedWriter(write, normalize, setError) {
34
35
  });
35
36
  };
36
37
  }
38
+ /**
39
+ * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery
40
+ * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),
41
+ * wired to the rail's own store delete. Only `delete` is dispatchable — the
42
+ * rails have no publish lifecycle and no bulk set-field — and an unsupported
43
+ * action fails each item HONESTLY in the report rather than silently no-oping
44
+ * (the exact bug this replaces: the assets rail's bulk delete confirmed into a
45
+ * missing `source.bulk` and dropped the whole action on the floor).
46
+ */
47
+ export function runRailBulkAction(input, ids, action, setField) {
48
+ return runBulkAction({
49
+ documents: input.documents,
50
+ titleOf: input.titleOf,
51
+ validate: input.validate,
52
+ // A rail store patches its own cache/state inside a succeeded remove
53
+ // (assets/users/redirects all do), and publish/unpublish/set-field never
54
+ // succeed here (dispatch rejects them), so the session-apply hooks have
55
+ // nothing left to apply.
56
+ applyState: () => undefined,
57
+ applyDelete: () => undefined,
58
+ dispatch: (bulkAction, ref) => bulkAction === 'delete'
59
+ ? input.removeConfirmed(ref)
60
+ : Promise.resolve({
61
+ ok: false,
62
+ error: {
63
+ code: 'unknown',
64
+ message: `Bulk ${bulkAction} is not supported in this view.`,
65
+ },
66
+ }),
67
+ }, input.collectionId, ids, action, setField);
68
+ }
37
69
  /**
38
70
  * Pure assembly of a {@link CollectionViewSource} from a rail's resolved
39
71
  * fields + state. Exported (and taking plain data/functions, no hook) so it is
@@ -42,7 +74,7 @@ export function createErrorMappedWriter(write, normalize, setError) {
42
74
  * wrapper around it.
43
75
  */
44
76
  export function assembleRailViewSource(input) {
45
- const { projectId, schema, capabilities, localeConfig, documents, get, titleOf, referenceCandidates, collections, detailPath, listPath, isFieldReadOnly, fieldAccess, validate, create, lastError, clearLastError, viewState, setViewState, save, remove, } = input;
77
+ const { projectId, schema, capabilities, localeConfig, documents, get, titleOf, referenceCandidates, collections, detailPath, listPath, isFieldReadOnly, fieldAccess, referenceTarget, validate, create, lastError, clearLastError, viewState, setViewState, save, remove, bulk, } = input;
46
78
  return {
47
79
  collectionId: schema.id,
48
80
  schema,
@@ -59,6 +91,7 @@ export function assembleRailViewSource(input) {
59
91
  ...(listPath !== undefined ? { listPath } : {}),
60
92
  isFieldReadOnly: isFieldReadOnly ?? ((field) => field.form?.readOnly ?? false),
61
93
  ...(fieldAccess ? { fieldAccess } : {}),
94
+ ...(referenceTarget ? { referenceTarget } : {}),
62
95
  validate,
63
96
  lastError,
64
97
  clearLastError,
@@ -67,6 +100,8 @@ export function assembleRailViewSource(input) {
67
100
  create,
68
101
  save,
69
102
  remove,
103
+ // Present iff the capability flag is set (the CollectionViewSource contract).
104
+ ...(capabilities.bulk && bulk ? { bulk } : {}),
70
105
  };
71
106
  }
72
107
  /**
@@ -78,7 +113,7 @@ export function assembleRailViewSource(input) {
78
113
  * here.
79
114
  */
80
115
  export function useRailViewSource(adapter) {
81
- const { projectId, schema, capabilities, localeConfig, documents, get, titleOf, referenceCandidates, collections, detailPath, listPath, isFieldReadOnly, fieldAccess, validate, create, save: rawSave, remove: rawRemove, normalizeSaveResult, normalizeRemoveResult, } = adapter;
116
+ const { projectId, schema, capabilities, localeConfig, documents, get, titleOf, referenceCandidates, collections, detailPath, listPath, isFieldReadOnly, fieldAccess, referenceTarget, validate, create, save: rawSave, remove: rawRemove, bulkRemove, normalizeSaveResult, normalizeRemoveResult, } = adapter;
82
117
  const [viewState, setViewStateRaw] = useState(() => defaultViewState(schema));
83
118
  const [lastError, setLastError] = useState(null);
84
119
  const setViewState = useCallback((update) => {
@@ -90,6 +125,21 @@ export function useRailViewSource(adapter) {
90
125
  normalizeSaveResult,
91
126
  ]);
92
127
  const remove = useCallback(createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError), [rawRemove, normalizeRemoveResult]);
128
+ // The rail bulk runner (EC-433): the shared `runBulkAction` machinery over
129
+ // the rail's own confirmed delete, its store result reconciled into an
130
+ // operation-shaped outcome (a failing remove is a visible per-item failure).
131
+ // Assembled onto the source only when `capabilities.bulk` is set (below).
132
+ const bulk = useCallback((ids, action, setField) => runRailBulkAction({
133
+ collectionId: schema.id,
134
+ documents: documents(),
135
+ titleOf,
136
+ validate,
137
+ removeConfirmed: async (ref) => {
138
+ const result = await (bulkRemove ?? rawRemove)(ref);
139
+ const error = normalizeRemoveResult(result);
140
+ return error ? { ok: false, error } : { ok: true, value: undefined };
141
+ },
142
+ }, ids, action, setField), [schema.id, documents, titleOf, validate, bulkRemove, rawRemove, normalizeRemoveResult]);
93
143
  return useMemo(() => assembleRailViewSource({
94
144
  projectId,
95
145
  schema,
@@ -104,6 +154,7 @@ export function useRailViewSource(adapter) {
104
154
  listPath,
105
155
  isFieldReadOnly,
106
156
  fieldAccess,
157
+ referenceTarget,
107
158
  validate,
108
159
  create,
109
160
  lastError,
@@ -112,6 +163,7 @@ export function useRailViewSource(adapter) {
112
163
  setViewState,
113
164
  save,
114
165
  remove,
166
+ bulk,
115
167
  }), [
116
168
  projectId,
117
169
  schema,
@@ -126,6 +178,7 @@ export function useRailViewSource(adapter) {
126
178
  listPath,
127
179
  isFieldReadOnly,
128
180
  fieldAccess,
181
+ referenceTarget,
129
182
  validate,
130
183
  create,
131
184
  lastError,
@@ -134,6 +187,7 @@ export function useRailViewSource(adapter) {
134
187
  setViewState,
135
188
  save,
136
189
  remove,
190
+ bulk,
137
191
  ]);
138
192
  }
139
193
  //# sourceMappingURL=rail-view-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAUtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAiClE;0EAC0E;AAC1E,MAAM,UAAU,iBAAiB,CAAC,MAAgB;IAChD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;AACxE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAA+B,EAC/B,cAAmC;IAEnC,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;IACjD,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;gFAEgF;AAChF,MAAM,UAAU,uBAAuB,CACrC,KAAsC,EACtC,SAAqD,EACrD,QAAyC;IAEzC,OAAO,CAAC,GAAS,EAAE,EAAE;QACnB,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YAC9B,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;YAC/B,IAAI,KAAK;gBAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;QAC5B,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC;AAiDD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAAkC;IACvE,MAAM,EACJ,SAAS,EACT,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,GAAG,EACH,OAAO,EACP,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,QAAQ,EACR,eAAe,EACf,WAAW,EACX,QAAQ,EACR,MAAM,EACN,SAAS,EACT,cAAc,EACd,SAAS,EACT,YAAY,EACZ,IAAI,EACJ,MAAM,GACP,GAAG,KAAK,CAAA;IACT,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,EAAE;QACvB,MAAM;QACN,YAAY;QACZ,SAAS;QACT,YAAY;QACZ,YAAY,EAAE,YAAY,CAAC,OAAO;QAClC,SAAS;QACT,GAAG;QACH,OAAO;QACP,mBAAmB,EAAE,mBAAmB,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QACtD,WAAW,EAAE,WAAW,IAAI,CAAC,MAAM,CAAC;QACpC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,eAAe,EAAE,eAAe,IAAI,CAAC,CAAC,KAAe,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,IAAI,KAAK,CAAC;QACxF,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,QAAQ;QACR,SAAS;QACT,cAAc;QACd,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS;QAC1B,YAAY;QACZ,MAAM;QACN,IAAI;QACJ,MAAM;KACP,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAgD;IAEhD,MAAM,EACJ,SAAS,EACT,MAAM,EACN,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,GAAG,EACH,OAAO,EACP,mBAAmB,EACnB,WAAW,EACX,UAAU,EACV,QAAQ,EACR,eAAe,EACf,WAAW,EACX,QAAQ,EACR,MAAM,EACN,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,SAAS,EACjB,mBAAmB,EACnB,qBAAqB,GACtB,GAAG,OAAO,CAAA;IAEX,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAiB,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAA;IAC7F,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAA;IAEvE,MAAM,YAAY,GAAG,WAAW,CAAuC,CAAC,MAAM,EAAE,EAAE;QAChF,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IACnF,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAEhE,MAAM,IAAI,GAAG,WAAW,CAAC,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,EAAE,YAAY,CAAC,EAAE;QAC5F,OAAO;QACP,mBAAmB;KACpB,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,WAAW,CACxB,uBAAuB,CAAC,SAAS,EAAE,qBAAqB,EAAE,YAAY,CAAC,EACvE,CAAC,SAAS,EAAE,qBAAqB,CAAC,CACnC,CAAA;IAED,OAAO,OAAO,CACZ,GAAG,EAAE,CACH,sBAAsB,CAAC;QACrB,SAAS;QACT,MAAM;QACN,YAAY;QACZ,YAAY;QACZ,SAAS;QACT,GAAG;QACH,OAAO;QACP,mBAAmB;QACnB,WAAW;QACX,UAAU;QACV,QAAQ;QACR,eAAe;QACf,WAAW;QACX,QAAQ;QACR,MAAM;QACN,SAAS;QACT,cAAc;QACd,SAAS;QACT,YAAY;QACZ,IAAI;QACJ,MAAM;KACP,CAAC,EACJ;QACE,SAAS;QACT,MAAM;QACN,YAAY;QACZ,YAAY;QACZ,SAAS;QACT,GAAG;QACH,OAAO;QACP,mBAAmB;QACnB,WAAW;QACX,UAAU;QACV,QAAQ;QACR,eAAe;QACf,WAAW;QACX,QAAQ;QACR,MAAM;QACN,SAAS;QACT,cAAc;QACd,SAAS;QACT,YAAY;QACZ,IAAI;QACJ,MAAM;KACP,CACF,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult>(\n write: (arg: TArg) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg) => void {\n return (arg: TArg) => {\n void write(arg).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n save(doc: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument): void\n remove(ref: DocumentRef): void\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n save,\n remove,\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n ],\n )\n}\n"]}
1
+ {"version":3,"file":"rail-view-source.js","sourceRoot":"","sources":["../../src/components/rail-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CmsValidationIssue,\n CollectionDef,\n DocumentRef,\n FieldDef,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { OperationError, OperationResult } from '@elytracms/core/operations'\nimport type { ReferenceTarget } from '@elytracms/core/persistence'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { runBulkAction } from './cms/bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './cms/bulk-actions'\n\n/**\n * The shared scaffold behind the primitive-rail `CollectionViewSource` hooks\n * (EC-327) — assets, users, redirects. All three hand-rolled identical\n * `viewState`/`lastError` state, an identical `setViewState` updater-fn\n * wrapper, an identical `if (!result.ok) setLastError(...)` error-mapping\n * shape (assets' `remove` diverges — see {@link OkResult} below), and a large\n * final `useMemo` assembling the same ~20-field {@link CollectionViewSource}.\n * The 4th rail (`useCmsCollectionViewSource`) does NOT reduplicate this — it\n * delegates to the ambient `CmsStore` — so this scaffold is scoped to the\n * three self-contained rails, not a fully generic replacement for the CMS\n * adapter.\n *\n * Genuinely divergent per-rail behaviour is a PARAMETER, never hidden here:\n * users' principal-based `fieldAccess`, users' no-op vs. redirects/assets'\n * registry-driven `validate`, redirects-only `create`, and — the one save/\n * remove result-shape mismatch — assets' `remove()` returns an\n * `outcome`-discriminated result where users/redirects use `{ ok }`; each rail\n * supplies its own `normalizeSaveResult`/`normalizeRemoveResult` to reconcile\n * whichever shape its store returns into `lastError`.\n */\n\n/** The common `{ ok: true; … } | { ok: false; message }` store-result shape —\n * users' and redirects' save/remove, and assets' `updateMetadata` (save). */\nexport type OkResult = { ok: true } | { ok: false; message: string }\n\n/** The default normalizer for an {@link OkResult}: a failure maps to a\n * surfaced {@link OperationError}; a success maps to no error at all. */\nexport function normalizeOkResult(result: OkResult): OperationError | null {\n return result.ok ? null : { code: 'unknown', message: result.message }\n}\n\n/**\n * Project a lens document's values into a meta bag (EC-311/EC-240/EC-303):\n * every entry whose key is NOT one of the rail's canonical/identity field\n * names round-trips through the record's open `meta` bag. Shared by the users\n * (`IDENTITY_FIELDS`) and redirects (`CANONICAL_REDIRECT_FIELDS`) rails — the\n * assets rail's projection is schema-FIELDS-driven inclusion (iterating the\n * schema's declared fields, not the document's value keys), a genuinely\n * different shape that stays bespoke in `assets-collection-view-source.ts`.\n */\nexport function metaBagProjection(\n values: Record<string, unknown>,\n canonicalNames: ReadonlySet<string>,\n): Record<string, unknown> {\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!canonicalNames.has(key)) meta[key] = value\n }\n return meta\n}\n\n/** Wrap a raw store write (save/remove) so a normalized failure surfaces via\n * `setError` — the factory's error-mapping plumbing, pulled out as a plain\n * function (no hook) so it is unit-testable without a `renderHook` harness. */\nexport function createErrorMappedWriter<TArg, TResult>(\n write: (arg: TArg) => Promise<TResult>,\n normalize: (result: TResult) => OperationError | null,\n setError: (error: OperationError) => void,\n): (arg: TArg) => void {\n return (arg: TArg) => {\n void write(arg).then((result) => {\n const error = normalize(result)\n if (error) setError(error)\n })\n }\n}\n\n/** The store-facing input of {@link runRailBulkAction} — plain data/functions,\n * so the rail bulk path is unit-testable without a hook harness. */\nexport interface RailBulkInput {\n collectionId: string\n /** Current rail rows (lens documents) — the runner's lookup + validation input. */\n documents: readonly CmsDocument[]\n titleOf(ref: DocumentRef): string\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Dispatch ONE already-confirmed delete (the EC-173 reference dialog ran\n * before anything reaches this) and reconcile the rail store's result shape\n * into an operation-shaped outcome — a failure here is a visible per-item\n * failure in the report, never a swallowed one.\n */\n removeConfirmed(ref: DocumentRef): Promise<OperationResult<unknown>>\n}\n\n/**\n * The primitive rails' bulk runner (EC-433): the SAME `runBulkAction` machinery\n * the CMS rail uses ([[client-data-flow-one-cache]] — no parallel data flow),\n * wired to the rail's own store delete. Only `delete` is dispatchable — the\n * rails have no publish lifecycle and no bulk set-field — and an unsupported\n * action fails each item HONESTLY in the report rather than silently no-oping\n * (the exact bug this replaces: the assets rail's bulk delete confirmed into a\n * missing `source.bulk` and dropped the whole action on the floor).\n */\nexport function runRailBulkAction(\n input: RailBulkInput,\n ids: readonly string[],\n action: BulkAction,\n setField?: BulkSetField,\n): Promise<BulkReport> {\n return runBulkAction(\n {\n documents: input.documents,\n titleOf: input.titleOf,\n validate: input.validate,\n // A rail store patches its own cache/state inside a succeeded remove\n // (assets/users/redirects all do), and publish/unpublish/set-field never\n // succeed here (dispatch rejects them), so the session-apply hooks have\n // nothing left to apply.\n applyState: () => undefined,\n applyDelete: () => undefined,\n dispatch: (bulkAction, ref) =>\n bulkAction === 'delete'\n ? input.removeConfirmed(ref)\n : Promise.resolve<OperationResult<unknown>>({\n ok: false,\n error: {\n code: 'unknown',\n message: `Bulk ${bulkAction} is not supported in this view.`,\n },\n }),\n },\n input.collectionId,\n ids,\n action,\n setField,\n )\n}\n\n/** The fields shared by {@link RailAdapter} and the pure assembly input —\n * everything the factory copies straight onto the assembled source, or\n * defaults when a rail omits it. */\ninterface RailViewSourceCore {\n readonly projectId: string\n readonly schema: CollectionDef\n readonly capabilities: CollectionViewCapabilities\n readonly localeConfig: LocaleConfig\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n titleOf(ref: DocumentRef): string\n /** Default `() => []` — none of the three rails carry cross-collection relation fields (v1). */\n referenceCandidates?(collectionId: string): CmsDocument[]\n /** Default `[schema]` — the rail's own schema is the only collection in scope. */\n collections?: readonly CollectionDef[]\n detailPath?(id: string): string\n listPath?: string\n /** Default `field => field.form?.readOnly ?? false`. */\n isFieldReadOnly?(field: FieldDef): boolean\n /** Principal-based tri-state — only the users rail supplies one. */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n /**\n * How this rail's records are addressed in the reference index (EC-433) —\n * only the assets rail supplies one (`{ type: 'asset' }`); absent ⇒ the\n * confirm dialog's default document-shaped lookup.\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n validate(doc: CmsDocument): CmsValidationIssue[]\n create(): DocumentRef\n}\n\n/** A rail's input to {@link useRailViewSource}: its store-facing reads/writes,\n * capability profile, and lens projections. `TSaveResult`/`TRemoveResult` are\n * each rail's own store-result shape — reconciled to `lastError` by the\n * rail's own `normalizeSaveResult`/`normalizeRemoveResult`. */\nexport interface RailAdapter<TSaveResult, TRemoveResult> extends RailViewSourceCore {\n save(doc: CmsDocument): Promise<TSaveResult>\n remove(ref: DocumentRef): Promise<TRemoveResult>\n /**\n * The delete the BULK path dispatches (EC-433) — always post-confirmation\n * (the EC-173 reference dialog ran first). Defaults to {@link remove}; the\n * assets rail overrides it to pass `confirmInUse: true` so a confirmed bulk\n * delete of a referenced asset actually deletes (mirroring the detail\n * surface's confirmed delete) instead of re-blocking on the usage gate.\n */\n bulkRemove?(ref: DocumentRef): Promise<TRemoveResult>\n normalizeSaveResult(result: TSaveResult): OperationError | null\n normalizeRemoveResult(result: TRemoveResult): OperationError | null\n}\n\n/** The already-resolved state + error-wrapped writes {@link useRailViewSource}\n * hands to the pure assembly function below. */\ninterface RailViewSourceAssemblyInput extends RailViewSourceCore {\n lastError: OperationError | null\n clearLastError(): void\n viewState: TableViewState\n setViewState: CollectionViewSource['setViewState']\n save(doc: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * The rail bulk runner (EC-433). Assembled onto the source ONLY when\n * `capabilities.bulk` is set — the `CollectionViewSource` contract is\n * \"method present iff the flag is\" — so a bulk-less rail's list never\n * half-offers the affordance.\n */\n bulk?: CollectionViewSource['bulk']\n}\n\n/**\n * Pure assembly of a {@link CollectionViewSource} from a rail's resolved\n * fields + state. Exported (and taking plain data/functions, no hook) so it is\n * directly unit-testable — this workspace runs vitest in `environment: 'node'`\n * (no `renderHook`); {@link useRailViewSource} is a thin `useState`/`useMemo`\n * wrapper around it.\n */\nexport function assembleRailViewSource(input: RailViewSourceAssemblyInput): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n } = input\n return {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig,\n activeLocale: localeConfig.default,\n documents,\n get,\n titleOf,\n referenceCandidates: referenceCandidates ?? (() => []),\n collections: collections ?? [schema],\n ...(detailPath ? { detailPath } : {}),\n ...(listPath !== undefined ? { listPath } : {}),\n isFieldReadOnly: isFieldReadOnly ?? ((field: FieldDef) => field.form?.readOnly ?? false),\n ...(fieldAccess ? { fieldAccess } : {}),\n ...(referenceTarget ? { referenceTarget } : {}),\n validate,\n lastError,\n clearLastError,\n viewState: () => viewState,\n setViewState,\n create,\n save,\n remove,\n // Present iff the capability flag is set (the CollectionViewSource contract).\n ...(capabilities.bulk && bulk ? { bulk } : {}),\n }\n}\n\n/**\n * The shared scaffold factory (EC-327) behind the assets/users/redirects rail\n * `CollectionViewSource` hooks: `viewState`/`lastError` state, the\n * `setViewState` updater-fn wrapper, the error-mapped save/remove wiring (via\n * the rail's own result normalizer), and the final assembled source. See the\n * module doc for what stays a per-rail parameter rather than being absorbed\n * here.\n */\nexport function useRailViewSource<TSaveResult, TRemoveResult>(\n adapter: RailAdapter<TSaveResult, TRemoveResult>,\n): CollectionViewSource {\n const {\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n save: rawSave,\n remove: rawRemove,\n bulkRemove,\n normalizeSaveResult,\n normalizeRemoveResult,\n } = adapter\n\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n const clearLastError = useCallback(() => setLastError(null), [])\n\n const save = useCallback(createErrorMappedWriter(rawSave, normalizeSaveResult, setLastError), [\n rawSave,\n normalizeSaveResult,\n ])\n const remove = useCallback(\n createErrorMappedWriter(rawRemove, normalizeRemoveResult, setLastError),\n [rawRemove, normalizeRemoveResult],\n )\n\n // The rail bulk runner (EC-433): the shared `runBulkAction` machinery over\n // the rail's own confirmed delete, its store result reconciled into an\n // operation-shaped outcome (a failing remove is a visible per-item failure).\n // Assembled onto the source only when `capabilities.bulk` is set (below).\n const bulk = useCallback(\n (ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport> =>\n runRailBulkAction(\n {\n collectionId: schema.id,\n documents: documents(),\n titleOf,\n validate,\n removeConfirmed: async (ref) => {\n const result = await (bulkRemove ?? rawRemove)(ref)\n const error = normalizeRemoveResult(result)\n return error ? { ok: false, error } : { ok: true, value: undefined }\n },\n },\n ids,\n action,\n setField,\n ),\n [schema.id, documents, titleOf, validate, bulkRemove, rawRemove, normalizeRemoveResult],\n )\n\n return useMemo(\n () =>\n assembleRailViewSource({\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n }),\n [\n projectId,\n schema,\n capabilities,\n localeConfig,\n documents,\n get,\n titleOf,\n referenceCandidates,\n collections,\n detailPath,\n listPath,\n isFieldReadOnly,\n fieldAccess,\n referenceTarget,\n validate,\n create,\n lastError,\n clearLastError,\n viewState,\n setViewState,\n save,\n remove,\n bulk,\n ],\n )\n}\n"]}
@@ -18,8 +18,9 @@ export declare function documentToSaveRedirect(doc: CmsDocument): SaveRedirectIn
18
18
  * the portable list/edit surfaces over the editorial redirects store instead of
19
19
  * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users
20
20
  * rail this source allows `create`, is NOT admin-only, and needs no principal
21
- * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect
22
- * is a small record saved explicitly (a saved+enabled one is live).
21
+ * field-access policy. No publish/versions/locales/inline/import — a redirect
22
+ * is a small record saved explicitly (a saved+enabled one is live); bulk
23
+ * delete rides the shared rail runner (EC-433).
23
24
  *
24
25
  * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).
25
26
  *
@@ -24,7 +24,7 @@ export const REDIRECT_COLLECTION_CAPABILITIES = {
24
24
  inlineEdit: false,
25
25
  create: true, // editors create redirects
26
26
  remove: true,
27
- bulk: false,
27
+ bulk: true, // EC-433: bulk delete = the same per-id remove, honestly reported
28
28
  liveUrl: false,
29
29
  hierarchy: false,
30
30
  importExport: false,
@@ -74,8 +74,9 @@ export function documentToSaveRedirect(doc) {
74
74
  * the portable list/edit surfaces over the editorial redirects store instead of
75
75
  * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users
76
76
  * rail this source allows `create`, is NOT admin-only, and needs no principal
77
- * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect
78
- * is a small record saved explicitly (a saved+enabled one is live).
77
+ * field-access policy. No publish/versions/locales/inline/import — a redirect
78
+ * is a small record saved explicitly (a saved+enabled one is live); bulk
79
+ * delete rides the shared rail runner (EC-433).
79
80
  *
80
81
  * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).
81
82
  *