@elytracms/studio 0.0.15 → 0.0.17
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/app-views/default-modules.d.ts +41 -0
- package/dist/components/app-views/default-modules.js +201 -0
- package/dist/components/app-views/default-modules.js.map +1 -0
- package/dist/components/app-views/module.d.ts +93 -0
- package/dist/components/app-views/module.js +75 -0
- package/dist/components/app-views/module.js.map +1 -0
- package/dist/components/app-views/sidebar.d.ts +95 -0
- package/dist/components/app-views/sidebar.js +153 -0
- package/dist/components/app-views/sidebar.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +7 -1
- package/dist/components/builder/builder-store.js +1 -1
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
- package/dist/components/builder/canvas-breakpoints.js +55 -0
- package/dist/components/builder/canvas-breakpoints.js.map +1 -0
- package/dist/components/builder/canvas.js +14 -8
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +18 -17
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +23 -5
- package/dist/components/cms/bulk-actions.js +7 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +22 -6
- package/dist/components/cms/bulk-bar.js +46 -9
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cms-index.d.ts +4 -2
- package/dist/components/cms/cms-index.js +6 -4
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +6 -5
- package/dist/components/cms/cms-store.js +51 -9
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -3
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/collections-sidebar.d.ts +10 -1
- package/dist/components/cms/collections-sidebar.js +13 -2
- package/dist/components/cms/collections-sidebar.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +11 -2
- package/dist/components/cms/field-control.js +22 -1
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +71 -0
- package/dist/components/cms/field-renderer.js +67 -0
- package/dist/components/cms/field-renderer.js.map +1 -0
- package/dist/components/cms/filter-builder.js +6 -28
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.d.ts +35 -0
- package/dist/components/cms/scalar-value-editor.js +43 -0
- package/dist/components/cms/scalar-value-editor.js.map +1 -0
- package/dist/components/project-session.js +8 -3
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/section-nav.d.ts +10 -10
- package/dist/components/section-nav.js +48 -25
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-surface.js +2 -3
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/studio-project-screen.d.ts +3 -4
- package/dist/components/studio-project-screen.js +29 -147
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +13 -9
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +19 -1
- package/dist/lib/data/collection-content.js +16 -0
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +27 -1
- package/dist/lib/workspace/index.js +34 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +22 -34
- package/dist/router/match.js +22 -40
- package/dist/router/match.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { FilterScalar } from '@elytracms/core/content';
|
|
3
|
+
import type { FilterableFieldOption } from '@elytracms/studio/lib/cms-views';
|
|
4
|
+
import type { BulkAction, BulkReport, BulkSetField } from './bulk-actions';
|
|
5
|
+
/**
|
|
6
|
+
* The scalar filterable fields a bulk set-field may target (EC-247): the
|
|
7
|
+
* schema's `filterable` fields whose type edits inline (text/number/boolean/
|
|
8
|
+
* select/date — `cellEditKind` 'inline'). Relation/asset (picker) and the
|
|
9
|
+
* publication-state pseudo field are excluded — publish/unpublish own state,
|
|
10
|
+
* and a raw relation id is not a safe bulk control.
|
|
11
|
+
*/
|
|
12
|
+
export declare function setFieldOptions(collection: CollectionDef): FilterableFieldOption[];
|
|
13
|
+
/** A sensible starting value for a freshly chosen set-field target. */
|
|
14
|
+
export declare function initialSetValue(option: FilterableFieldOption | undefined): FilterScalar;
|
|
2
15
|
/**
|
|
3
16
|
* The bulk-action bar of the collection table (EC-169): appears with a row
|
|
4
|
-
* selection, offers publish/unpublish/delete
|
|
5
|
-
* confirmed by the owner through the
|
|
6
|
-
* (`ReferenceConfirmDialog`), which enumerates
|
|
7
|
-
* will break before anything dispatches — this bar
|
|
17
|
+
* selection, offers publish/unpublish/delete and a set-field assignment
|
|
18
|
+
* (EC-247) over it. Unpublish and delete are confirmed by the owner through the
|
|
19
|
+
* reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates
|
|
20
|
+
* every inbound reference that will break before anything dispatches — this bar
|
|
21
|
+
* only raises the action. Set-field is a draft edit, so it applies directly.
|
|
8
22
|
*/
|
|
9
|
-
export declare function BulkBar({ count, busy, onAction, onClear, }: {
|
|
23
|
+
export declare function BulkBar({ collection, count, busy, onAction, onSetField, onClear, }: {
|
|
24
|
+
collection: CollectionDef;
|
|
10
25
|
/** Selected rows in the current view. */
|
|
11
26
|
count: number;
|
|
12
27
|
busy: boolean;
|
|
13
28
|
onAction: (action: BulkAction) => void;
|
|
29
|
+
onSetField: (set: BulkSetField) => void;
|
|
14
30
|
onClear: () => void;
|
|
15
31
|
}): import("react").JSX.Element | null;
|
|
16
32
|
/**
|
|
@@ -1,18 +1,54 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Check, CheckCircle2, CircleSlash, PencilLine, Trash2, X } from 'lucide-react';
|
|
4
|
+
import { Badge, Button, Code, Row, Select, Spacer, Stack, Text } from '@elytracms/studio/ui';
|
|
5
|
+
import { filterableFieldOptions } from '@elytracms/studio/lib/cms-views';
|
|
4
6
|
import { bulkSummary } from './bulk-actions';
|
|
7
|
+
import { cellEditKind } from './cell-edit';
|
|
8
|
+
import { ScalarFieldValueEditor } from './scalar-value-editor';
|
|
9
|
+
/**
|
|
10
|
+
* The scalar filterable fields a bulk set-field may target (EC-247): the
|
|
11
|
+
* schema's `filterable` fields whose type edits inline (text/number/boolean/
|
|
12
|
+
* select/date — `cellEditKind` 'inline'). Relation/asset (picker) and the
|
|
13
|
+
* publication-state pseudo field are excluded — publish/unpublish own state,
|
|
14
|
+
* and a raw relation id is not a safe bulk control.
|
|
15
|
+
*/
|
|
16
|
+
export function setFieldOptions(collection) {
|
|
17
|
+
return filterableFieldOptions(collection).filter((option) => option.def !== null && cellEditKind(option.def, false) === 'inline');
|
|
18
|
+
}
|
|
19
|
+
/** A sensible starting value for a freshly chosen set-field target. */
|
|
20
|
+
export function initialSetValue(option) {
|
|
21
|
+
return option?.def?.type === 'boolean' ? false : '';
|
|
22
|
+
}
|
|
5
23
|
/**
|
|
6
24
|
* The bulk-action bar of the collection table (EC-169): appears with a row
|
|
7
|
-
* selection, offers publish/unpublish/delete
|
|
8
|
-
* confirmed by the owner through the
|
|
9
|
-
* (`ReferenceConfirmDialog`), which enumerates
|
|
10
|
-
* will break before anything dispatches — this bar
|
|
25
|
+
* selection, offers publish/unpublish/delete and a set-field assignment
|
|
26
|
+
* (EC-247) over it. Unpublish and delete are confirmed by the owner through the
|
|
27
|
+
* reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates
|
|
28
|
+
* every inbound reference that will break before anything dispatches — this bar
|
|
29
|
+
* only raises the action. Set-field is a draft edit, so it applies directly.
|
|
11
30
|
*/
|
|
12
|
-
export function BulkBar({ count, busy, onAction, onClear, }) {
|
|
31
|
+
export function BulkBar({ collection, count, busy, onAction, onSetField, onClear, }) {
|
|
32
|
+
const [setMode, setSetMode] = useState(false);
|
|
13
33
|
if (count === 0)
|
|
14
34
|
return null;
|
|
15
|
-
|
|
35
|
+
const options = setFieldOptions(collection);
|
|
36
|
+
const settable = options.length > 0;
|
|
37
|
+
return (_jsxs(Stack, { gap: "none", className: "shrink-0", "data-testid": "bulk-bar", children: [_jsxs(Row, { gap: "2", className: "border-b border-border bg-muted/40 px-4 py-2 sm:px-6", children: [_jsxs(Text, { size: "sm", weight: "medium", "data-testid": "bulk-count", children: [count, " selected"] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-publish", onClick: () => onAction('publish'), children: [_jsx(CheckCircle2, {}), " Publish"] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-unpublish", onClick: () => onAction('unpublish'), children: [_jsx(CircleSlash, {}), " Unpublish"] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy || !settable, "aria-pressed": setMode, title: settable ? undefined : 'No filterable scalar fields to set.', "data-testid": "bulk-set-field", onClick: () => setSetMode((prev) => !prev), children: [_jsx(PencilLine, {}), " Set field"] }), _jsxs(Button, { variant: "destructive", size: "sm", disabled: busy, "data-testid": "bulk-delete", onClick: () => onAction('delete'), children: [_jsx(Trash2, {}), " Delete"] }), _jsx(Spacer, {}), _jsxs(Button, { variant: "ghost", size: "sm", disabled: busy, "data-testid": "bulk-clear", onClick: onClear, children: [_jsx(X, {}), " Clear selection"] })] }), setMode && settable ? (_jsx(SetFieldPanel, { options: options, busy: busy, onApply: (set) => {
|
|
38
|
+
onSetField(set);
|
|
39
|
+
setSetMode(false);
|
|
40
|
+
}, onCancel: () => setSetMode(false) })) : null] }));
|
|
41
|
+
}
|
|
42
|
+
/** The inline "set <field> to <value>" composer over the current selection. */
|
|
43
|
+
function SetFieldPanel({ options, busy, onApply, onCancel, }) {
|
|
44
|
+
const [field, setField] = useState(options[0].field);
|
|
45
|
+
const option = options.find((candidate) => candidate.field === field) ?? options[0];
|
|
46
|
+
const [value, setValue] = useState(() => initialSetValue(option));
|
|
47
|
+
function changeField(next) {
|
|
48
|
+
setField(next);
|
|
49
|
+
setValue(initialSetValue(options.find((candidate) => candidate.field === next)));
|
|
50
|
+
}
|
|
51
|
+
return (_jsxs(Row, { gap: "2", align: "center", className: "border-b border-border bg-muted/20 px-4 py-2 sm:px-6", "data-testid": "bulk-set-panel", children: [_jsx(Text, { size: "sm", tone: "muted", children: "Set" }), _jsx(Select, { "aria-label": "Set field", value: field, onChange: (event) => changeField(event.target.value), className: "h-7 w-auto py-0 text-xs", "data-testid": "bulk-set-field-select", children: options.map((candidate) => (_jsx("option", { value: candidate.field, children: candidate.label }, candidate.field))) }), _jsx(Text, { size: "sm", tone: "muted", children: "to" }), _jsx(ScalarFieldValueEditor, { option: option, value: value, ariaLabel: "Set value", testId: "bulk-set-value", onChange: setValue }, field), _jsx(Spacer, {}), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy, "data-testid": "bulk-set-apply", onClick: () => onApply({ field, value }), children: [_jsx(Check, {}), " Apply to selection"] }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy, "data-testid": "bulk-set-cancel", onClick: onCancel, children: "Cancel" })] }));
|
|
16
52
|
}
|
|
17
53
|
/**
|
|
18
54
|
* Per-item bulk results (EC-169): the completion summary plus every failure,
|
|
@@ -21,6 +57,7 @@ export function BulkBar({ count, busy, onAction, onClear, }) {
|
|
|
21
57
|
export function BulkReportPanel({ report, onDismiss, }) {
|
|
22
58
|
const { succeeded, failed } = bulkSummary(report);
|
|
23
59
|
const failures = report.results.filter((result) => !result.ok);
|
|
24
|
-
|
|
60
|
+
const actionLabel = report.action === 'set-field' && report.setField ? `set ${report.setField.field}` : report.action;
|
|
61
|
+
return (_jsxs(Stack, { gap: "1", className: "shrink-0 border-b border-border px-4 py-2 sm:px-6", "data-testid": "bulk-report", children: [_jsxs(Row, { gap: "2", children: [_jsxs(Text, { size: "sm", weight: "medium", "data-testid": "bulk-report-summary", children: ["Bulk ", actionLabel, ": ", succeeded, " succeeded", failed > 0 ? `, ${failed} failed` : ''] }), failed > 0 ? _jsxs(Badge, { variant: "error", children: [failed, " failed"] }) : _jsx(Badge, { variant: "success", children: "done" }), _jsx(Spacer, {}), _jsx(Button, { variant: "ghost", size: "sm", "data-testid": "bulk-report-dismiss", onClick: onDismiss, children: "Dismiss" })] }), failures.map((failure) => (_jsxs(Row, { gap: "2", "data-testid": `bulk-failure-${failure.ref.id}`, children: [_jsx(Text, { size: "xs", className: "text-destructive", children: failure.title }), _jsx(Code, { tone: "muted", children: failure.ref.id }), _jsx(Text, { size: "xs", tone: "muted", children: failure.reason })] }, failure.ref.id)))] }));
|
|
25
62
|
}
|
|
26
63
|
//# sourceMappingURL=bulk-bar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-bar.js","sourceRoot":"","sources":["../../../src/components/cms/bulk-bar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACpF,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAG5C;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,EACtB,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,OAAO,GAOR;IACC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAE5B,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,+DAA+D,iBAAa,UAAU,aAC3G,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,QAAQ,iBAAa,YAAY,aACrD,KAAK,iBACD,EACP,MAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,QAAQ,EAAE,IAAI,iBACF,cAAc,EAC1B,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,aAElC,KAAC,YAAY,KAAG,gBACT,EACT,MAAC,MAAM,IACL,OAAO,EAAC,SAAS,EACjB,IAAI,EAAC,IAAI,EACT,QAAQ,EAAE,IAAI,iBACF,gBAAgB,EAC5B,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,aAEpC,KAAC,WAAW,KAAG,kBACR,EACT,MAAC,MAAM,IACL,OAAO,EAAC,aAAa,EACrB,IAAI,EAAC,IAAI,EACT,QAAQ,EAAE,IAAI,iBACF,aAAa,EACzB,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAEjC,KAAC,MAAM,KAAG,eACH,EACT,KAAC,MAAM,KAAG,EACV,MAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,EAAE,IAAI,iBAAc,YAAY,EAAC,OAAO,EAAE,OAAO,aACzF,KAAC,CAAC,KAAG,wBACE,IACL,CACP,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,MAAM,EACN,SAAS,GAIV;IACC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,CAAA;IACjD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAE9D,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,mDAAmD,iBAAa,aAAa,aACpG,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,QAAQ,iBAAa,qBAAqB,sBACzD,MAAM,CAAC,MAAM,QAAI,SAAS,gBAC/B,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,IAClC,EACN,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAC,KAAK,IAAC,OAAO,EAAC,OAAO,aAAE,MAAM,eAAgB,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,qBAAa,EACpG,KAAC,MAAM,KAAG,EACV,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,iBAAa,qBAAqB,EAAC,OAAO,EAAE,SAAS,wBAE7E,IACL,EACL,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACzB,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,iBAAmC,gBAAgB,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,aAC7E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,YACzC,OAAO,CAAC,KAAK,GACT,EACP,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,GAAQ,EAC1C,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,OAAO,CAAC,MAAM,GACV,KAPS,OAAO,CAAC,GAAG,CAAC,EAAE,CAQ1B,CACP,CAAC,IACI,CACT,CAAA;AACH,CAAC","sourcesContent":["import { CheckCircle2, CircleSlash, Trash2, X } from 'lucide-react'\nimport { Badge, Button, Code, Row, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { bulkSummary } from './bulk-actions'\nimport type { BulkAction, BulkReport } from './bulk-actions'\n\n/**\n * The bulk-action bar of the collection table (EC-169): appears with a row\n * selection, offers publish/unpublish/delete over it. Unpublish and delete are\n * confirmed by the owner through the reference-aware EC-173 dialog\n * (`ReferenceConfirmDialog`), which enumerates every inbound reference that\n * will break before anything dispatches — this bar only raises the action.\n */\nexport function BulkBar({\n count,\n busy,\n onAction,\n onClear,\n}: {\n /** Selected rows in the current view. */\n count: number\n busy: boolean\n onAction: (action: BulkAction) => void\n onClear: () => void\n}) {\n if (count === 0) return null\n\n return (\n <Row gap=\"2\" className=\"shrink-0 border-b border-border bg-muted/40 px-4 py-2 sm:px-6\" data-testid=\"bulk-bar\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-count\">\n {count} selected\n </Text>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-publish\"\n onClick={() => onAction('publish')}\n >\n <CheckCircle2 /> Publish\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-unpublish\"\n onClick={() => onAction('unpublish')}\n >\n <CircleSlash /> Unpublish\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-delete\"\n onClick={() => onAction('delete')}\n >\n <Trash2 /> Delete\n </Button>\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-clear\" onClick={onClear}>\n <X /> Clear selection\n </Button>\n </Row>\n )\n}\n\n/**\n * Per-item bulk results (EC-169): the completion summary plus every failure,\n * named and with its reason — a partial failure is enumerated, never silent.\n */\nexport function BulkReportPanel({\n report,\n onDismiss,\n}: {\n report: BulkReport\n onDismiss: () => void\n}) {\n const { succeeded, failed } = bulkSummary(report)\n const failures = report.results.filter((result) => !result.ok)\n\n return (\n <Stack gap=\"1\" className=\"shrink-0 border-b border-border px-4 py-2 sm:px-6\" data-testid=\"bulk-report\">\n <Row gap=\"2\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-report-summary\">\n Bulk {report.action}: {succeeded} succeeded\n {failed > 0 ? `, ${failed} failed` : ''}\n </Text>\n {failed > 0 ? <Badge variant=\"error\">{failed} failed</Badge> : <Badge variant=\"success\">done</Badge>}\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"bulk-report-dismiss\" onClick={onDismiss}>\n Dismiss\n </Button>\n </Row>\n {failures.map((failure) => (\n <Row gap=\"2\" key={failure.ref.id} data-testid={`bulk-failure-${failure.ref.id}`}>\n <Text size=\"xs\" className=\"text-destructive\">\n {failure.title}\n </Text>\n <Code tone=\"muted\">{failure.ref.id}</Code>\n <Text size=\"xs\" tone=\"muted\">\n {failure.reason}\n </Text>\n </Row>\n ))}\n </Stack>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bulk-bar.js","sourceRoot":"","sources":["../../../src/components/cms/bulk-bar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { Check, CheckCircle2, CircleSlash, PencilLine, Trash2, X } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport { Badge, Button, Code, Row, Select, Spacer, Stack, Text } from '@elytracms/studio/ui'\nimport { filterableFieldOptions } from '@elytracms/studio/lib/cms-views'\nimport type { FilterableFieldOption } from '@elytracms/studio/lib/cms-views'\nimport { bulkSummary } from './bulk-actions'\nimport type { BulkAction, BulkReport, BulkSetField } from './bulk-actions'\nimport { cellEditKind } from './cell-edit'\nimport { ScalarFieldValueEditor } from './scalar-value-editor'\n\n/**\n * The scalar filterable fields a bulk set-field may target (EC-247): the\n * schema's `filterable` fields whose type edits inline (text/number/boolean/\n * select/date — `cellEditKind` 'inline'). Relation/asset (picker) and the\n * publication-state pseudo field are excluded — publish/unpublish own state,\n * and a raw relation id is not a safe bulk control.\n */\nexport function setFieldOptions(collection: CollectionDef): FilterableFieldOption[] {\n return filterableFieldOptions(collection).filter(\n (option) => option.def !== null && cellEditKind(option.def, false) === 'inline',\n )\n}\n\n/** A sensible starting value for a freshly chosen set-field target. */\nexport function initialSetValue(option: FilterableFieldOption | undefined): FilterScalar {\n return option?.def?.type === 'boolean' ? false : ''\n}\n\n/**\n * The bulk-action bar of the collection table (EC-169): appears with a row\n * selection, offers publish/unpublish/delete and a set-field assignment\n * (EC-247) over it. Unpublish and delete are confirmed by the owner through the\n * reference-aware EC-173 dialog (`ReferenceConfirmDialog`), which enumerates\n * every inbound reference that will break before anything dispatches — this bar\n * only raises the action. Set-field is a draft edit, so it applies directly.\n */\nexport function BulkBar({\n collection,\n count,\n busy,\n onAction,\n onSetField,\n onClear,\n}: {\n collection: CollectionDef\n /** Selected rows in the current view. */\n count: number\n busy: boolean\n onAction: (action: BulkAction) => void\n onSetField: (set: BulkSetField) => void\n onClear: () => void\n}) {\n const [setMode, setSetMode] = useState(false)\n if (count === 0) return null\n\n const options = setFieldOptions(collection)\n const settable = options.length > 0\n\n return (\n <Stack gap=\"none\" className=\"shrink-0\" data-testid=\"bulk-bar\">\n <Row gap=\"2\" className=\"border-b border-border bg-muted/40 px-4 py-2 sm:px-6\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-count\">\n {count} selected\n </Text>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-publish\"\n onClick={() => onAction('publish')}\n >\n <CheckCircle2 /> Publish\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-unpublish\"\n onClick={() => onAction('unpublish')}\n >\n <CircleSlash /> Unpublish\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy || !settable}\n aria-pressed={setMode}\n title={settable ? undefined : 'No filterable scalar fields to set.'}\n data-testid=\"bulk-set-field\"\n onClick={() => setSetMode((prev) => !prev)}\n >\n <PencilLine /> Set field\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-delete\"\n onClick={() => onAction('delete')}\n >\n <Trash2 /> Delete\n </Button>\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-clear\" onClick={onClear}>\n <X /> Clear selection\n </Button>\n </Row>\n {setMode && settable ? (\n <SetFieldPanel\n options={options}\n busy={busy}\n onApply={(set) => {\n onSetField(set)\n setSetMode(false)\n }}\n onCancel={() => setSetMode(false)}\n />\n ) : null}\n </Stack>\n )\n}\n\n/** The inline \"set <field> to <value>\" composer over the current selection. */\nfunction SetFieldPanel({\n options,\n busy,\n onApply,\n onCancel,\n}: {\n options: FilterableFieldOption[]\n busy: boolean\n onApply: (set: BulkSetField) => void\n onCancel: () => void\n}) {\n const [field, setField] = useState(options[0]!.field)\n const option = options.find((candidate) => candidate.field === field) ?? options[0]!\n const [value, setValue] = useState<FilterScalar>(() => initialSetValue(option))\n\n function changeField(next: string) {\n setField(next)\n setValue(initialSetValue(options.find((candidate) => candidate.field === next)))\n }\n\n return (\n <Row\n gap=\"2\"\n align=\"center\"\n className=\"border-b border-border bg-muted/20 px-4 py-2 sm:px-6\"\n data-testid=\"bulk-set-panel\"\n >\n <Text size=\"sm\" tone=\"muted\">\n Set\n </Text>\n <Select\n aria-label=\"Set field\"\n value={field}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid=\"bulk-set-field-select\"\n >\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Text size=\"sm\" tone=\"muted\">\n to\n </Text>\n <ScalarFieldValueEditor\n key={field}\n option={option}\n value={value}\n ariaLabel=\"Set value\"\n testId=\"bulk-set-value\"\n onChange={setValue}\n />\n <Spacer />\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={busy}\n data-testid=\"bulk-set-apply\"\n onClick={() => onApply({ field, value })}\n >\n <Check /> Apply to selection\n </Button>\n <Button variant=\"ghost\" size=\"sm\" disabled={busy} data-testid=\"bulk-set-cancel\" onClick={onCancel}>\n Cancel\n </Button>\n </Row>\n )\n}\n\n/**\n * Per-item bulk results (EC-169): the completion summary plus every failure,\n * named and with its reason — a partial failure is enumerated, never silent.\n */\nexport function BulkReportPanel({\n report,\n onDismiss,\n}: {\n report: BulkReport\n onDismiss: () => void\n}) {\n const { succeeded, failed } = bulkSummary(report)\n const failures = report.results.filter((result) => !result.ok)\n const actionLabel =\n report.action === 'set-field' && report.setField ? `set ${report.setField.field}` : report.action\n\n return (\n <Stack gap=\"1\" className=\"shrink-0 border-b border-border px-4 py-2 sm:px-6\" data-testid=\"bulk-report\">\n <Row gap=\"2\">\n <Text size=\"sm\" weight=\"medium\" data-testid=\"bulk-report-summary\">\n Bulk {actionLabel}: {succeeded} succeeded\n {failed > 0 ? `, ${failed} failed` : ''}\n </Text>\n {failed > 0 ? <Badge variant=\"error\">{failed} failed</Badge> : <Badge variant=\"success\">done</Badge>}\n <Spacer />\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"bulk-report-dismiss\" onClick={onDismiss}>\n Dismiss\n </Button>\n </Row>\n {failures.map((failure) => (\n <Row gap=\"2\" key={failure.ref.id} data-testid={`bulk-failure-${failure.ref.id}`}>\n <Text size=\"xs\" className=\"text-destructive\">\n {failure.title}\n </Text>\n <Code tone=\"muted\">{failure.ref.id}</Code>\n <Text size=\"xs\" tone=\"muted\">\n {failure.reason}\n </Text>\n </Row>\n ))}\n </Stack>\n )\n}\n"]}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The CMS section landing view (EC-223 Stage 6): the collection grid, shown when
|
|
3
3
|
* no collection is selected. Takes `projectId` as a prop; the splat resolver
|
|
4
|
-
* mounts it inside the CMS provider so {@link useCms} resolves.
|
|
4
|
+
* mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`
|
|
5
|
+
* (EC-239) narrows the grid for a focused CMS module instance.
|
|
5
6
|
*/
|
|
6
|
-
export declare function CmsIndex({ projectId }: {
|
|
7
|
+
export declare function CmsIndex({ projectId, focusCollections, }: {
|
|
7
8
|
projectId: string;
|
|
9
|
+
focusCollections?: readonly string[];
|
|
8
10
|
}): import("react").JSX.Element;
|
|
@@ -3,16 +3,18 @@ import { Database, FileCog } from 'lucide-react';
|
|
|
3
3
|
import { isSingleton } from '@elytracms/core/cms-core';
|
|
4
4
|
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
5
5
|
import { useCms } from '@elytracms/studio/components/cms/cms-store';
|
|
6
|
-
import { CollectionsSidebar } from '@elytracms/studio/components/cms/collections-sidebar';
|
|
6
|
+
import { CollectionsSidebar, focusCollectionList, } from '@elytracms/studio/components/cms/collections-sidebar';
|
|
7
7
|
import { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui';
|
|
8
8
|
/**
|
|
9
9
|
* The CMS section landing view (EC-223 Stage 6): the collection grid, shown when
|
|
10
10
|
* no collection is selected. Takes `projectId` as a prop; the splat resolver
|
|
11
|
-
* mounts it inside the CMS provider so {@link useCms} resolves.
|
|
11
|
+
* mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`
|
|
12
|
+
* (EC-239) narrows the grid for a focused CMS module instance.
|
|
12
13
|
*/
|
|
13
|
-
export function CmsIndex({ projectId }) {
|
|
14
|
+
export function CmsIndex({ projectId, focusCollections, }) {
|
|
14
15
|
const cms = useCms();
|
|
15
|
-
|
|
16
|
+
const collections = focusCollectionList(cms.collections, focusCollections);
|
|
17
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-3xl p-4 sm:p-6", children: [_jsx("div", { className: "mb-6 h-72 overflow-hidden rounded-lg border border-border md:hidden", children: _jsx(CollectionsSidebar, { projectId: projectId, focusCollections: focusCollections }) }), _jsxs(Stack, { gap: "6", children: [_jsxs(Stack, { gap: "1", children: [_jsx(Heading, { size: "display", children: "Content" }), _jsx(Text, { tone: "muted", children: "Schema-driven collections. Open a collection to browse its documents as a table and edit them in the detail editor." })] }), _jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: collections.map((collection) => {
|
|
16
18
|
// EC-217: a singleton reads as one fixed document — a distinct icon
|
|
17
19
|
// and a "Single document" note instead of a (always-1) row count.
|
|
18
20
|
const singleton = isSingleton(collection);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cms-index.js","sourceRoot":"","sources":["../../../src/components/cms/cms-index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,4CAA4C,CAAA;AACnE,OAAO,
|
|
1
|
+
{"version":3,"file":"cms-index.js","sourceRoot":"","sources":["../../../src/components/cms/cms-index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,MAAM,EAAE,MAAM,4CAA4C,CAAA;AACnE,OAAO,EACL,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,sDAAsD,CAAA;AAC7D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,EACvB,SAAS,EACT,gBAAgB,GAIjB;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,WAAW,GAAG,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAA;IAE1E,OAAO,CACL,eAAK,SAAS,EAAC,qCAAqC,aAClD,cAAK,SAAS,EAAC,qEAAqE,YAClF,KAAC,kBAAkB,IAAC,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,GAAI,GAC5E,EACN,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,OAAO,IAAC,IAAI,EAAC,SAAS,wBAAkB,EACzC,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,oIAGX,IACD,EACR,cAAK,SAAS,EAAC,2BAA2B,YACvC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;4BAC9B,oEAAoE;4BACpE,kEAAkE;4BAClE,MAAM,SAAS,GAAG,WAAW,CAAC,UAAU,CAAC,CAAA;4BACzC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;4BAC3C,OAAO,CACL,KAAC,IAAI,IAEH,EAAE,EAAE,oBAAoB,EACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,YAEtE,MAAC,IAAI,IAAC,SAAS,EAAC,uEAAuE,aACrF,eAAM,SAAS,EAAC,mFAAmF,YACjG,KAAC,IAAI,IAAC,SAAS,EAAC,QAAQ,GAAG,GACtB,EACP,MAAC,KAAK,IAAC,GAAG,EAAC,MAAM,EAAC,SAAS,EAAC,SAAS,aACnC,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,EAAC,SAAS,EAAC,UAAU,YACzC,UAAU,CAAC,IAAI,EAAE,KAAK,IAAI,UAAU,CAAC,EAAE,GACnC,EACP,MAAC,IAAI,IAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,OAAO,aACjC,UAAU,CAAC,EAAE,cAAK,UAAU,CAAC,MAAM,CAAC,MAAM,eACtC,IACD,EACR,KAAC,MAAM,KAAG,EACT,SAAS,CAAC,CAAC,CAAC,CACX,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,mBAAmB,gCAEnD,CACR,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,IAAI,kBAC9B,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,MAAM,GACjC,CACR,IACI,IA1BF,UAAU,CAAC,EAAE,CA2Bb,CACR,CAAA;wBACH,CAAC,CAAC,GACE,IACA,IACJ,CACP,CAAA;AACH,CAAC","sourcesContent":["import { Database, FileCog } from 'lucide-react'\nimport { isSingleton } from '@elytracms/core/cms-core'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { useCms } from '@elytracms/studio/components/cms/cms-store'\nimport {\n CollectionsSidebar,\n focusCollectionList,\n} from '@elytracms/studio/components/cms/collections-sidebar'\nimport { Card, Code, Heading, Spacer, Stack, Text } from '@elytracms/studio/ui'\n\n/**\n * The CMS section landing view (EC-223 Stage 6): the collection grid, shown when\n * no collection is selected. Takes `projectId` as a prop; the splat resolver\n * mounts it inside the CMS provider so {@link useCms} resolves. `focusCollections`\n * (EC-239) narrows the grid for a focused CMS module instance.\n */\nexport function CmsIndex({\n projectId,\n focusCollections,\n}: {\n projectId: string\n focusCollections?: readonly string[]\n}) {\n const cms = useCms()\n const collections = focusCollectionList(cms.collections, focusCollections)\n\n return (\n <div className=\"mx-auto w-full max-w-3xl p-4 sm:p-6\">\n <div className=\"mb-6 h-72 overflow-hidden rounded-lg border border-border md:hidden\">\n <CollectionsSidebar projectId={projectId} focusCollections={focusCollections} />\n </div>\n <Stack gap=\"6\">\n <Stack gap=\"1\">\n <Heading size=\"display\">Content</Heading>\n <Text tone=\"muted\">\n Schema-driven collections. Open a collection to browse its documents as a table and edit\n them in the detail editor.\n </Text>\n </Stack>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {collections.map((collection) => {\n // EC-217: a singleton reads as one fixed document — a distinct icon\n // and a \"Single document\" note instead of a (always-1) row count.\n const singleton = isSingleton(collection)\n const Icon = singleton ? FileCog : Database\n return (\n <Link\n key={collection.id}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsCollection(collection.id) }}\n >\n <Card className=\"flex items-center gap-3 p-4 transition-colors hover:border-primary/30\">\n <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n <Icon className=\"size-4\" />\n </span>\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text weight=\"semibold\" className=\"truncate\">\n {collection.form?.label ?? collection.id}\n </Text>\n <Code tone=\"muted\" className=\"w-fit\">\n {collection.id} · {collection.fields.length} fields\n </Code>\n </Stack>\n <Spacer />\n {singleton ? (\n <Text size=\"xs\" tone=\"muted\" className=\"whitespace-nowrap\">\n Single document\n </Text>\n ) : (\n <Text size=\"sm\" tone=\"muted\" mono>\n {cms.documentsOf(collection.id).length}\n </Text>\n )}\n </Card>\n </Link>\n )\n })}\n </div>\n </Stack>\n </div>\n )\n}\n"]}
|
|
@@ -4,7 +4,7 @@ import type { DocumentVersionRecord } from '@elytracms/core/persistence';
|
|
|
4
4
|
import type { OperationError } from '@elytracms/studio/lib/operations';
|
|
5
5
|
import type { TableViewState } from '@elytracms/studio/lib/cms-views';
|
|
6
6
|
import type { CellCommitInput, CellCommitOutcome } from './cell-edit';
|
|
7
|
-
import type { BulkAction, BulkReport } from './bulk-actions';
|
|
7
|
+
import type { BulkAction, BulkReport, BulkSetField } from './bulk-actions';
|
|
8
8
|
/**
|
|
9
9
|
* A saved table view (EC-169, supersedes the EC-071 column presets): a named
|
|
10
10
|
* bundle of filters (closed AD-3 vocabulary), sort, and visible/ordered
|
|
@@ -134,11 +134,12 @@ export interface CmsStore {
|
|
|
134
134
|
/** Delete a named view — a view-only operation; documents are never touched. */
|
|
135
135
|
deleteView(collectionId: string, viewId: string): Promise<boolean>;
|
|
136
136
|
/**
|
|
137
|
-
* Bulk publish/unpublish/delete (EC-169): one operations
|
|
138
|
-
* per-item outcomes in the report — partial failure is
|
|
139
|
-
* silent. Only succeeded items mutate the session.
|
|
137
|
+
* Bulk publish/unpublish/delete (EC-169) + set-field (EC-247): one operations
|
|
138
|
+
* command PER item, per-item outcomes in the report — partial failure is
|
|
139
|
+
* enumerated, never silent. Only succeeded items mutate the session. A
|
|
140
|
+
* `set-field` carries its `{ field, value }` payload.
|
|
140
141
|
*/
|
|
141
|
-
runBulk(collectionId: string, ids: readonly string[], action: BulkAction): Promise<BulkReport>;
|
|
142
|
+
runBulk(collectionId: string, ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>;
|
|
142
143
|
/**
|
|
143
144
|
* Mirror a collection into the session (e.g. an accepted AI-generated
|
|
144
145
|
* schema, EC-097). Replaces by id. Session-only: persistence happens through
|
|
@@ -9,7 +9,7 @@ import { slugify } from '@elytracms/studio/lib/utils';
|
|
|
9
9
|
import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
10
10
|
import { ALL_VIEW_ID, canonicalViewState, defaultViewState, deleteSavedView, loadSavedViews, newViewId, normalizeViewState, saveSavedView, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
|
|
11
11
|
import { useOptionalAssets } from '../assets/assets-store';
|
|
12
|
-
import { commitCellEdit } from './cell-edit';
|
|
12
|
+
import { applyCellValue, commitCellEdit } from './cell-edit';
|
|
13
13
|
import { runBulkAction } from './bulk-actions';
|
|
14
14
|
/**
|
|
15
15
|
* Mint a collision-safe, slug-derived page id (EC-192): the slug of the page name,
|
|
@@ -583,7 +583,27 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
583
583
|
return true;
|
|
584
584
|
}, [dispatchViewMutation, activeViews, dropTableState]);
|
|
585
585
|
// --- Bulk actions (EC-169) ------------------------------------------------
|
|
586
|
-
const runBulk = useCallback(async (collectionId, ids, action) => {
|
|
586
|
+
const runBulk = useCallback(async (collectionId, ids, action, setField) => {
|
|
587
|
+
const collection = registry.get(collectionId);
|
|
588
|
+
// A set-field needs both the field payload AND the collection def to fold
|
|
589
|
+
// the value the locale-aware way — guard so a misconfigured call fails
|
|
590
|
+
// loud rather than silently no-opping each item.
|
|
591
|
+
if (action === 'set-field' && (!setField || !collection)) {
|
|
592
|
+
return {
|
|
593
|
+
action,
|
|
594
|
+
...(setField ? { setField } : {}),
|
|
595
|
+
results: ids.map((id) => ({
|
|
596
|
+
ref: { collection: collectionId, id },
|
|
597
|
+
title: titleOf({ collection: collectionId, id }),
|
|
598
|
+
ok: false,
|
|
599
|
+
reason: 'Set-field is missing its field/value or the collection schema.',
|
|
600
|
+
error: { code: 'unknown', message: 'Invalid set-field request.' },
|
|
601
|
+
})),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
// The next document for a set-field item: fold the value into the active
|
|
605
|
+
// locale's bag (localized) or the shared `values` (non-localized).
|
|
606
|
+
const setFieldDoc = (doc) => applyCellValue(collection, doc, setField.field, activeLocale, setField.value);
|
|
587
607
|
let dispatch;
|
|
588
608
|
const persister = persisterRef.current;
|
|
589
609
|
if (persister && projectId !== undefined) {
|
|
@@ -591,11 +611,30 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
591
611
|
// command per item (same ordering contract as commitFieldEdit).
|
|
592
612
|
await persister.ensureBaseline();
|
|
593
613
|
const { operations } = await getStudioOperations();
|
|
594
|
-
dispatch = (bulkAction, ref) =>
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
614
|
+
dispatch = (bulkAction, ref) => {
|
|
615
|
+
if (bulkAction === 'delete')
|
|
616
|
+
return operations.documents.delete({ projectId, ref });
|
|
617
|
+
if (bulkAction === 'publish')
|
|
618
|
+
return operations.documents.publish({ projectId, ref });
|
|
619
|
+
if (bulkAction === 'unpublish')
|
|
620
|
+
return operations.documents.unpublish({ projectId, ref });
|
|
621
|
+
// set-field: upsert the document carrying the new value. The write
|
|
622
|
+
// path self-types it through the schema lens (EC-277); a scalar field
|
|
623
|
+
// stores its scalar as-is.
|
|
624
|
+
const current = documentsRef.current.find((d) => d.collection === ref.collection && d.id === ref.id);
|
|
625
|
+
if (!current) {
|
|
626
|
+
return Promise.resolve({
|
|
627
|
+
ok: false,
|
|
628
|
+
error: {
|
|
629
|
+
code: 'not-found',
|
|
630
|
+
message: 'Document is no longer in the session.',
|
|
631
|
+
entity: 'cms-document',
|
|
632
|
+
key: `${ref.collection}:${ref.id}`,
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return operations.documents.upsert({ projectId, document: setFieldDoc(current) });
|
|
637
|
+
};
|
|
599
638
|
}
|
|
600
639
|
return runBulkAction({
|
|
601
640
|
documents: documentsRef.current,
|
|
@@ -617,9 +656,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
617
656
|
// pointer set).
|
|
618
657
|
applyState: (ref, state) => setPublished(ref, state === 'published'),
|
|
619
658
|
applyDelete: (ref) => setDocuments((prev) => prev.filter((d) => !(d.collection === ref.collection && d.id === ref.id))),
|
|
659
|
+
// EC-247: fold the new value into the session document (same
|
|
660
|
+
// locale-aware path the dispatched upsert used).
|
|
661
|
+
applySetField: (ref) => setDocuments((prev) => prev.map((d) => d.collection === ref.collection && d.id === ref.id ? setFieldDoc(d) : d)),
|
|
620
662
|
...(dispatch ? { dispatch } : {}),
|
|
621
|
-
}, collectionId, ids, action);
|
|
622
|
-
}, [projectId, titleOf, registry, localeConfig, setPublished, assetExists]);
|
|
663
|
+
}, collectionId, ids, action, setField);
|
|
664
|
+
}, [projectId, titleOf, registry, localeConfig, activeLocale, setPublished, assetExists]);
|
|
623
665
|
const store$ = useMemo(() => ({
|
|
624
666
|
...(projectId !== undefined ? { projectId } : {}),
|
|
625
667
|
collections,
|