@elytracms/studio 0.0.68 → 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.
- package/dist/components/assets/asset-detail.d.ts +4 -2
- package/dist/components/assets/asset-detail.js +4 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +30 -19
- package/dist/components/assets/assets-collection-view-source.js +55 -23
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +6 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +17 -4
- package/dist/components/builder/selection-overlay.js +17 -15
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +26 -1
- package/dist/components/builder/spacing-handles.js +66 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +11 -1
- package/dist/components/cms/bulk-bar.js +10 -3
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/collection-table.js +19 -2
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/field-control.js +5 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +5 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +5 -1
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/rich-text/editor.d.ts +24 -5
- package/dist/components/cms/rich-text/editor.js +23 -8
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +50 -1
- package/dist/components/rail-view-source.js +56 -2
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +3 -2
- package/dist/components/redirects/redirects-collection-view-source.js +4 -3
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
- package/dist/components/users/users-collection-view-source.js +1 -1
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +12 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/dnd/gap-drag.d.ts +8 -1
- package/dist/lib/dnd/gap-drag.js +4 -0
- package/dist/lib/dnd/gap-drag.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +2 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +1 -0
- package/dist/lib/i18n/messages.en.js +3 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/references/references-store.d.ts +8 -1
- package/dist/lib/references/references-store.js +12 -2
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/ui/surface-geometry.d.ts +8 -5
- package/dist/ui/surface-geometry.js +8 -5
- package/dist/ui/surface-geometry.js.map +1 -1
- package/package.json +2 -2
|
@@ -30,7 +30,7 @@ import { RichTextToolbar } from './toolbar.js';
|
|
|
30
30
|
* The image and component-embed pickers are owned here (not the toolbar) so both the
|
|
31
31
|
* toolbar buttons and the `/` slash menu open the same modal — one insertion path.
|
|
32
32
|
*/
|
|
33
|
-
export function RichTextEditor({ value, onChange, readOnly = false, id, allow }) {
|
|
33
|
+
export function RichTextEditor({ value, onChange, readOnly = false, id, allow, label }) {
|
|
34
34
|
// Ambient context for reference-aware insertion (null outside a collection view /
|
|
35
35
|
// asset backend — the editor then degrades to external links and no image insert).
|
|
36
36
|
const source = useOptionalCollectionView();
|
|
@@ -133,7 +133,7 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
|
|
|
133
133
|
// While expanded, the toolbar stays put above the scrolling prose column
|
|
134
134
|
// (the scroll area is the EditorContent wrapper).
|
|
135
135
|
const surface = (_jsxs(_Fragment, { children: [!readOnly ? (_jsx(RichTextToolbar, { editor: editor, targets: linkTargets, onOpenImage: openImage, onOpenEmbed: openEmbed, expanded: expanded, onToggleExpand: () => setExpanded((v) => !v) })) : null, _jsx(EditorContent, { editor: editor, className: cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS) })] }));
|
|
136
|
-
return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [expanded ? (_jsxs(_Fragment, { children: [_jsx(RichTextExpandPlaceholder, { onCollapse: () => setExpanded(false) }), _jsx(RichTextExpandStage, { onCollapse: () => setExpanded(false), children: surface })] })) : (surface), canImage ? (_jsx(AssetPicker, { open: imageOpen, onOpenChange: setImageOpen, value: null, accept: "image/*", onChange: (assetId) => {
|
|
136
|
+
return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [expanded ? (_jsxs(_Fragment, { children: [_jsx(RichTextExpandPlaceholder, { onCollapse: () => setExpanded(false) }), _jsx(RichTextExpandStage, { label: label, onCollapse: () => setExpanded(false), children: surface })] })) : (surface), canImage ? (_jsx(AssetPicker, { open: imageOpen, onOpenChange: setImageOpen, value: null, accept: "image/*", onChange: (assetId) => {
|
|
137
137
|
setImageOpen(false);
|
|
138
138
|
if (assetId)
|
|
139
139
|
insertAssetImage(editor, assetId, assets);
|
|
@@ -142,13 +142,15 @@ export function RichTextEditor({ value, onChange, readOnly = false, id, allow })
|
|
|
142
142
|
/**
|
|
143
143
|
* EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be
|
|
144
144
|
* wider than the text (that is the point — distraction-free writing, not more
|
|
145
|
-
* pixels), so the PROSE COLUMN inside is constrained to
|
|
146
|
-
*
|
|
147
|
-
*
|
|
145
|
+
* pixels), so the PROSE COLUMN inside is constrained to a readable measure and
|
|
146
|
+
* centered with `mx-auto`, which keeps the gutters symmetric at every stage
|
|
147
|
+
* width. EC-436 widened the measure from the classic ~70ch to ~78ch in step
|
|
148
|
+
* with the wider `prose` stage panel (owner feedback: "the dialog + text looks
|
|
149
|
+
* a bit narrow on a big screen"). Applied to the `EditorContent` wrapper only
|
|
148
150
|
* while expanded; it is also the scroll area, so the toolbar above it stays
|
|
149
151
|
* put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).
|
|
150
152
|
*/
|
|
151
|
-
export const RICH_TEXT_STAGE_CONTENT_CLASS = 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[
|
|
153
|
+
export const RICH_TEXT_STAGE_CONTENT_CLASS = 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[78ch]';
|
|
152
154
|
/**
|
|
153
155
|
* The inline strip shown in the field row while the editor is expanded onto its
|
|
154
156
|
* stage (the real editing surface lives there then). Clicking it returns — so
|
|
@@ -157,6 +159,19 @@ export const RICH_TEXT_STAGE_CONTENT_CLASS = 'min-h-0 flex-1 overflow-y-auto px-
|
|
|
157
159
|
export function RichTextExpandPlaceholder({ onCollapse }) {
|
|
158
160
|
return (_jsxs("button", { type: "button", onClick: onCollapse, className: "flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40", children: [_jsx(Minimize2, { className: "size-4" }), _jsx(Text, { size: "sm", tone: "muted", children: translate('richText.expand.editingInSheet') })] }));
|
|
159
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* EC-436 (AD-1) — the expand stage's title. With a field label in reach it
|
|
164
|
+
* composes "<label> — <localized type>" through the PARAMETERIZED
|
|
165
|
+
* `richText.expand.titleWithField` message — never string concatenation in
|
|
166
|
+
* code, because word order is a translation concern; without one (a custom
|
|
167
|
+
* mount that passes no label) it falls back to the generic
|
|
168
|
+
* `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.
|
|
169
|
+
*/
|
|
170
|
+
export function richTextStageTitle(label) {
|
|
171
|
+
return label !== undefined && label.trim() !== ''
|
|
172
|
+
? translate('richText.expand.titleWithField', { field: label })
|
|
173
|
+
: translate('richText.expand.title');
|
|
174
|
+
}
|
|
160
175
|
/**
|
|
161
176
|
* EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the
|
|
162
177
|
* centered STAGE (ratified AD-B — writing is a primary activity, so it takes
|
|
@@ -174,7 +189,7 @@ export function RichTextExpandPlaceholder({ onCollapse }) {
|
|
|
174
189
|
* the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape
|
|
175
190
|
* first (the stage stays), per the EC-392 stacking rules.
|
|
176
191
|
*/
|
|
177
|
-
function RichTextExpandStage({ children, onCollapse, }) {
|
|
178
|
-
return (_jsx(ExpandableSurface, { title:
|
|
192
|
+
function RichTextExpandStage({ children, onCollapse, label, }) {
|
|
193
|
+
return (_jsx(ExpandableSurface, { title: richTextStageTitle(label), closeLabel: translate('richText.expand.collapse'), variant: "stage", preset: "prose", onDismiss: onCollapse, children: _jsx("div", { className: "ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background", children: children }) }));
|
|
179
194
|
}
|
|
180
195
|
//# sourceMappingURL=editor.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n richTextValueFromDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : richTextValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage onCollapse={() => setExpanded(false)}>{surface}</RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept=\"image/*\"\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to the classic readable\n * measure (~70ch) and centered with `mx-auto`, which keeps the gutters\n * symmetric at every stage width. Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[70ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n}: {\n children: ReactNode\n onCollapse: () => void\n}) {\n return (\n <ExpandableSurface\n title={translate('richText.expand.title')}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\n </div>\n </ExpandableSurface>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { Minimize2 } from 'lucide-react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { ExpandableSurface, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n richTextValueFromDoc,\n toEditorDoc,\n} from './value'\nimport { insertAssetImage, insertComponentEmbed, listEmbeddable } from './insert'\nimport { EmbedPicker } from './embed-picker'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n /**\n * EC-436 (AD-1) — the field's resolved localized display label, the SAME\n * string the field/prop row renders next to this editor. Used only to title\n * the expand stage (\"<label> — Rich text\" via the parameterized\n * `richText.expand.titleWithField` message, {@link richTextStageTitle});\n * absent → the generic `richText.expand.title` fallback.\n */\n label?: string\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n *\n * The image and component-embed pickers are owned here (not the toolbar) so both the\n * toolbar buttons and the `/` slash menu open the same modal — one insertion path.\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow, label }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n const [imageOpen, setImageOpen] = useState(false)\n const [embedOpen, setEmbedOpen] = useState(false)\n // EC-388/EC-431: whether the editor is promoted onto its centered expand stage.\n const [expanded, setExpanded] = useState(false)\n\n const canImage = assets != null\n // The `allow`-gated components this field may embed — drives both the toolbar\n // picker and the slash menu's direct per-component items (0.0.41).\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n const canEmbed = embeddable.length > 0\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n // Media openers for the toolbar + slash menu. setState fns are stable, so these\n // stay valid for the life of the editor even though it's built once.\n const openImage = canImage ? () => setImageOpen(true) : undefined\n const openEmbed = canEmbed ? () => setEmbedOpen(true) : undefined\n\n const editor = useEditor({\n extensions: buildRichTextExtensions({\n slash: {\n onImage: openImage,\n // Direct per-component slash items (0.0.41) — insert with manifest defaults\n // and select, so the embed inspector opens onto the new node's props.\n embed: canEmbed\n ? {\n components: embeddable,\n insert: (ed, range, manifest) => {\n ed.chain().focus().deleteRange(range).run()\n insertComponentEmbed(ed, manifest, { select: true })\n },\n }\n : undefined,\n },\n }),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : richTextValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n // The editing surface — the toolbar (which carries the expand toggle) + the\n // editable content. Mounted inline OR inside the expand stage, never both. TipTap\n // preserves the editor instance across `EditorContent` remounts (its view DOM is\n // carried onto the new element), so relocating this surface keeps ONE editor\n // document and ONE value flow — the same instance, grown, not a second form.\n // While expanded, the toolbar stays put above the scrolling prose column\n // (the scroll area is the EditorContent wrapper).\n const surface = (\n <>\n {!readOnly ? (\n <RichTextToolbar\n editor={editor}\n targets={linkTargets}\n onOpenImage={openImage}\n onOpenEmbed={openEmbed}\n expanded={expanded}\n onToggleExpand={() => setExpanded((v) => !v)}\n />\n ) : null}\n <EditorContent editor={editor} className={cn(expanded && RICH_TEXT_STAGE_CONTENT_CLASS)} />\n </>\n )\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {expanded ? (\n <>\n <RichTextExpandPlaceholder onCollapse={() => setExpanded(false)} />\n <RichTextExpandStage label={label} onCollapse={() => setExpanded(false)}>\n {surface}\n </RichTextExpandStage>\n </>\n ) : (\n surface\n )}\n {canImage ? (\n <AssetPicker\n open={imageOpen}\n onOpenChange={setImageOpen}\n value={null}\n accept=\"image/*\"\n onChange={(assetId) => {\n setImageOpen(false)\n if (assetId) insertAssetImage(editor, assetId, assets)\n }}\n />\n ) : null}\n {canEmbed ? (\n <EmbedPicker\n open={embedOpen}\n onOpenChange={setEmbedOpen}\n allow={allow}\n onSelect={(manifest) => insertComponentEmbed(editor, manifest, { select: true })}\n />\n ) : null}\n </div>\n )\n}\n\n/**\n * EC-431 (AD-B) — the expanded editing surface's scroll body: the stage may be\n * wider than the text (that is the point — distraction-free writing, not more\n * pixels), so the PROSE COLUMN inside is constrained to a readable measure and\n * centered with `mx-auto`, which keeps the gutters symmetric at every stage\n * width. EC-436 widened the measure from the classic ~70ch to ~78ch in step\n * with the wider `prose` stage panel (owner feedback: \"the dialog + text looks\n * a bit narrow on a big screen\"). Applied to the `EditorContent` wrapper only\n * while expanded; it is also the scroll area, so the toolbar above it stays\n * put. Pinned by `expand.test.tsx` (the EC-391 class-contract pattern).\n */\nexport const RICH_TEXT_STAGE_CONTENT_CLASS =\n 'min-h-0 flex-1 overflow-y-auto px-6 py-6 sm:px-10 sm:py-8 [&>.ec-rte-content]:mx-auto [&>.ec-rte-content]:max-w-[78ch]'\n\n/**\n * The inline strip shown in the field row while the editor is expanded onto its\n * stage (the real editing surface lives there then). Clicking it returns — so\n * the collapsed field never appears as a broken empty box.\n */\nexport function RichTextExpandPlaceholder({ onCollapse }: { onCollapse: () => void }) {\n return (\n <button\n type=\"button\"\n onClick={onCollapse}\n className=\"flex min-h-[8rem] w-full items-center justify-center gap-2 px-3 py-6 text-sm text-muted-foreground transition-colors hover:bg-muted/40\"\n >\n <Minimize2 className=\"size-4\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('richText.expand.editingInSheet')}\n </Text>\n </button>\n )\n}\n\n/**\n * EC-436 (AD-1) — the expand stage's title. With a field label in reach it\n * composes \"<label> — <localized type>\" through the PARAMETERIZED\n * `richText.expand.titleWithField` message — never string concatenation in\n * code, because word order is a translation concern; without one (a custom\n * mount that passes no label) it falls back to the generic\n * `richText.expand.title`, unchanged. Exported for the `expand.test.tsx` pins.\n */\nexport function richTextStageTitle(label?: string): string {\n return label !== undefined && label.trim() !== ''\n ? translate('richText.expand.titleWithField', { field: label })\n : translate('richText.expand.title')\n}\n\n/**\n * EC-388/EC-431 — the expanded editing surface: the SAME editor, grown onto the\n * centered STAGE (ratified AD-B — writing is a primary activity, so it takes\n * the `stage` prose preset, never the peripheral side sheet) where long prose\n * (a privacy statement, a Freeform block) is comfortable to edit. It renders\n * through the shared {@link ExpandableSurface} primitive (portal-to-body,\n * backdrop, header close, layer-stacked Escape); the prose column inside is\n * constrained to the distraction-free measure\n * ({@link RICH_TEXT_STAGE_CONTENT_CLASS}). It is the same edit surface, not a\n * second form: no save/cancel of its own — edits flow through the editor's\n * existing value contract, and it is lightly dismissible (Escape / backdrop /\n * the toolbar's collapse toggle / the header close), the forms-never-modal\n * spirit applied. The `children` are the live toolbar + `EditorContent`;\n * unmounting them here returns the editor inline. The asset picker opened from\n * the stage's toolbar is a Radix dialog ABOVE the stage — it consumes Escape\n * first (the stage stays), per the EC-392 stacking rules.\n */\nfunction RichTextExpandStage({\n children,\n onCollapse,\n label,\n}: {\n children: ReactNode\n onCollapse: () => void\n /** The field's resolved display label — see {@link richTextStageTitle}. */\n label?: string\n}) {\n return (\n <ExpandableSurface\n title={richTextStageTitle(label)}\n closeLabel={translate('richText.expand.collapse')}\n variant=\"stage\"\n preset=\"prose\"\n onDismiss={onCollapse}\n >\n <div className=\"ec-rte flex min-h-0 flex-1 flex-col overflow-hidden bg-background\">\n {children}\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":"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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 } 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/
|
|
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:
|
|
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/
|
|
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
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The capability profile of the redirects rail (EC-303): content-gated, create\n * + remove, no publish/versions/locales — redirects are CONTENT (an editor\n * owns them), so unlike the users rail this source allows `create`, is NOT\n * admin-only, and needs no principal field-access policy. Hoisted to a\n * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.\n */\nexport const REDIRECT_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta = metaBagProjection(values, CANONICAL_REDIRECT_FIELDS)\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the redirects-store\n * reads/writes, the capability profile, and the registry-driven `validate`.\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => store.redirects.map(redirectToDocument), [store.redirects])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n [registry],\n )\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback((doc: CmsDocument) => store.save(documentToSaveRedirect(doc)), [store])\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: REDIRECT_COLLECTION_CAPABILITIES,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections` override: redirects carry no\n // relation/asset fields, so nothing cross-collection to resolve (the\n // factory's `() => []` / `[schema]` defaults apply).\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo } from 'react'\nimport type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\nimport { metaBagProjection, normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The capability profile of the redirects rail (EC-303): content-gated, create\n * + remove, no publish/versions/locales — redirects are CONTENT (an editor\n * owns them), so unlike the users rail this source allows `create`, is NOT\n * admin-only, and needs no principal field-access policy. Hoisted to a\n * module-level constant (EC-327), mirroring `ASSET_COLLECTION_CAPABILITIES`.\n */\nexport const REDIRECT_COLLECTION_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: true, // EC-433: bulk delete = the same per-id remove, honestly reported\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta = metaBagProjection(values, CANONICAL_REDIRECT_FIELDS)\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live); bulk\n * delete rides the shared rail runner (EC-433).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n *\n * The shared scaffold (`useRailViewSource`, EC-327) owns `viewState`/`lastError`\n * plumbing and the assembled source; this hook supplies only the redirects-store\n * reads/writes, the capability profile, and the registry-driven `validate`.\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => store.redirects.map(redirectToDocument), [store.redirects])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const validate = useCallback(\n (doc: CmsDocument) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n [registry],\n )\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback((doc: CmsDocument) => store.save(documentToSaveRedirect(doc)), [store])\n const remove = useCallback((ref: DocumentRef) => store.remove(ref.id), [store])\n\n return useRailViewSource({\n projectId,\n schema,\n capabilities: REDIRECT_COLLECTION_CAPABILITIES,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // No `referenceCandidates`/`collections` override: redirects carry no\n // relation/asset fields, so nothing cross-collection to resolve (the\n // factory's `() => []` / `[schema]` defaults apply).\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n}\n"]}
|
|
@@ -21,7 +21,7 @@ export const USER_COLLECTION_CAPABILITIES = {
|
|
|
21
21
|
inlineEdit: false,
|
|
22
22
|
create: false, // identities are minted by sign-up, never here
|
|
23
23
|
remove: true, // revoke membership (does not delete the auth identity)
|
|
24
|
-
bulk:
|
|
24
|
+
bulk: true, // EC-433: bulk delete = the same per-id revoke, honestly reported
|
|
25
25
|
liveUrl: false,
|
|
26
26
|
hierarchy: false,
|
|
27
27
|
importExport: false, // credential records are never dumped to raw JSON
|