@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.
Files changed (69) hide show
  1. package/dist/components/app-views/default-modules.d.ts +41 -0
  2. package/dist/components/app-views/default-modules.js +201 -0
  3. package/dist/components/app-views/default-modules.js.map +1 -0
  4. package/dist/components/app-views/module.d.ts +93 -0
  5. package/dist/components/app-views/module.js +75 -0
  6. package/dist/components/app-views/module.js.map +1 -0
  7. package/dist/components/app-views/sidebar.d.ts +95 -0
  8. package/dist/components/app-views/sidebar.js +153 -0
  9. package/dist/components/app-views/sidebar.js.map +1 -0
  10. package/dist/components/builder/builder-store.d.ts +7 -1
  11. package/dist/components/builder/builder-store.js +1 -1
  12. package/dist/components/builder/builder-store.js.map +1 -1
  13. package/dist/components/builder/canvas-breakpoints.d.ts +53 -0
  14. package/dist/components/builder/canvas-breakpoints.js +55 -0
  15. package/dist/components/builder/canvas-breakpoints.js.map +1 -0
  16. package/dist/components/builder/canvas.js +14 -8
  17. package/dist/components/builder/canvas.js.map +1 -1
  18. package/dist/components/builder/composition-field-editor.js +18 -17
  19. package/dist/components/builder/composition-field-editor.js.map +1 -1
  20. package/dist/components/cms/bulk-actions.d.ts +23 -5
  21. package/dist/components/cms/bulk-actions.js +7 -3
  22. package/dist/components/cms/bulk-actions.js.map +1 -1
  23. package/dist/components/cms/bulk-bar.d.ts +22 -6
  24. package/dist/components/cms/bulk-bar.js +46 -9
  25. package/dist/components/cms/bulk-bar.js.map +1 -1
  26. package/dist/components/cms/cms-index.d.ts +4 -2
  27. package/dist/components/cms/cms-index.js +6 -4
  28. package/dist/components/cms/cms-index.js.map +1 -1
  29. package/dist/components/cms/cms-store.d.ts +6 -5
  30. package/dist/components/cms/cms-store.js +51 -9
  31. package/dist/components/cms/cms-store.js.map +1 -1
  32. package/dist/components/cms/collection-table.js +7 -3
  33. package/dist/components/cms/collection-table.js.map +1 -1
  34. package/dist/components/cms/collections-sidebar.d.ts +10 -1
  35. package/dist/components/cms/collections-sidebar.js +13 -2
  36. package/dist/components/cms/collections-sidebar.js.map +1 -1
  37. package/dist/components/cms/field-control.d.ts +11 -2
  38. package/dist/components/cms/field-control.js +22 -1
  39. package/dist/components/cms/field-control.js.map +1 -1
  40. package/dist/components/cms/field-renderer.d.ts +71 -0
  41. package/dist/components/cms/field-renderer.js +67 -0
  42. package/dist/components/cms/field-renderer.js.map +1 -0
  43. package/dist/components/cms/filter-builder.js +6 -28
  44. package/dist/components/cms/filter-builder.js.map +1 -1
  45. package/dist/components/cms/scalar-value-editor.d.ts +35 -0
  46. package/dist/components/cms/scalar-value-editor.js +43 -0
  47. package/dist/components/cms/scalar-value-editor.js.map +1 -0
  48. package/dist/components/project-session.js +8 -3
  49. package/dist/components/project-session.js.map +1 -1
  50. package/dist/components/section-nav.d.ts +10 -10
  51. package/dist/components/section-nav.js +48 -25
  52. package/dist/components/section-nav.js.map +1 -1
  53. package/dist/components/section-surface.js +2 -3
  54. package/dist/components/section-surface.js.map +1 -1
  55. package/dist/components/studio-project-screen.d.ts +3 -4
  56. package/dist/components/studio-project-screen.js +29 -147
  57. package/dist/components/studio-project-screen.js.map +1 -1
  58. package/dist/components/studio-shell.js +13 -9
  59. package/dist/components/studio-shell.js.map +1 -1
  60. package/dist/lib/data/collection-content.d.ts +19 -1
  61. package/dist/lib/data/collection-content.js +16 -0
  62. package/dist/lib/data/collection-content.js.map +1 -1
  63. package/dist/lib/workspace/index.d.ts +27 -1
  64. package/dist/lib/workspace/index.js +34 -0
  65. package/dist/lib/workspace/index.js.map +1 -1
  66. package/dist/router/match.d.ts +22 -34
  67. package/dist/router/match.js +22 -40
  68. package/dist/router/match.js.map +1 -1
  69. package/package.json +2 -2
@@ -1,16 +1,32 @@
1
- import type { BulkAction, BulkReport } from './bulk-actions';
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 over it. Unpublish and delete are
5
- * confirmed by the owner through the reference-aware EC-173 dialog
6
- * (`ReferenceConfirmDialog`), which enumerates every inbound reference that
7
- * will break before anything dispatches — this bar only raises the action.
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 { CheckCircle2, CircleSlash, Trash2, X } from 'lucide-react';
3
- import { Badge, Button, Code, Row, Spacer, Stack, Text } from '@elytracms/studio/ui';
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 over it. Unpublish and delete are
8
- * confirmed by the owner through the reference-aware EC-173 dialog
9
- * (`ReferenceConfirmDialog`), which enumerates every inbound reference that
10
- * will break before anything dispatches — this bar only raises the action.
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
- return (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-border bg-muted/40 px-4 py-2 sm:px-6", "data-testid": "bulk-bar", 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: "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"] })] }));
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
- 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 ", report.action, ": ", 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)))] }));
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":"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","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
- 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 }) }), _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: cms.collections.map((collection) => {
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,EAAE,kBAAkB,EAAE,MAAM,sDAAsD,CAAA;AACzF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAE/E;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,EAAE,SAAS,EAAyB;IAC3D,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IAEpB,OAAO,CACL,eAAK,SAAS,EAAC,qCAAqC,aAClD,cAAK,SAAS,EAAC,qEAAqE,YAClF,KAAC,kBAAkB,IAAC,SAAS,EAAE,SAAS,GAAI,GACxC,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,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE;4BAClC,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 { CollectionsSidebar } 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.\n */\nexport function CmsIndex({ projectId }: { projectId: string }) {\n const cms = useCms()\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} />\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 {cms.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"]}
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 command PER item,
138
- * per-item outcomes in the report — partial failure is enumerated, never
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) => bulkAction === 'delete'
595
- ? operations.documents.delete({ projectId, ref })
596
- : bulkAction === 'publish'
597
- ? operations.documents.publish({ projectId, ref })
598
- : operations.documents.unpublish({ projectId, ref });
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,