@elytracms/studio 0.0.49 → 0.0.51

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.
@@ -1,25 +1,33 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { ListFilter, X } from 'lucide-react';
4
4
  import { Button, Popover, PopoverAnchor, PopoverContent, Row, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
5
5
  import { cn } from '@elytracms/studio/lib/utils';
6
6
  import { translate } from '@elytracms/studio/lib/i18n';
7
- import { OPERATOR_LABELS, STATE_FIELD, defaultClause, filterableFieldOptions, operatorsFor, scalarListFromText, scalarListToText, validateClauses, } from '@elytracms/studio/lib/cms-views';
7
+ import { STATE_FIELD, defaultClause, filterableFieldOptions, operatorChoices, operatorLabelKey, scalarListFromText, scalarListToText, validateClauses, } from '@elytracms/studio/lib/cms-views';
8
8
  import { useCollectionView } from '../collection-view/collection-view-context';
9
+ import { RelationPicker } from './relation-picker';
9
10
  import { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor';
11
+ const defaultDescribeValue = (clause) => Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value);
10
12
  /**
11
- * A one-line summary of the active filter for the chip face (EC-246b): the
12
- * single condition read out ("Status equals news"), or "N filters" for several.
13
+ * A one-line summary of the active filter for the chip face (EC-246b): the single
14
+ * condition read out ("Status is news", "Slug contains hello", "Author is Ada"), or
15
+ * "N filters" for several. Operator labels are type-aware and localized (EC-345);
16
+ * `empty` reads as just "… is empty" with no value; `describeValue` lets the caller
17
+ * swap raw scalars (e.g. a relation id) for a human title.
13
18
  */
14
- export function summarizeFilters(filters, options) {
19
+ export function summarizeFilters(filters, options, describeValue = defaultDescribeValue) {
15
20
  if (filters.length === 0)
16
21
  return translate('cms.filter.button');
17
22
  if (filters.length > 1)
18
23
  return translate('cms.filter.summaryMultiple', { count: filters.length });
19
24
  const clause = filters[0];
20
- const label = options.find((option) => option.field === clause.field)?.label ?? clause.field;
21
- const value = Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value);
22
- return `${label} ${OPERATOR_LABELS[clause.op]} ${value}`.trim();
25
+ const option = options.find((candidate) => candidate.field === clause.field);
26
+ const label = option?.label ?? clause.field;
27
+ const opLabel = translate(operatorLabelKey(clause.op, option, clause.value));
28
+ if (clause.op === 'empty')
29
+ return `${label} ${opLabel}`.trim();
30
+ return `${label} ${opLabel} ${describeValue(clause, option)}`.trim();
23
31
  }
24
32
  /**
25
33
  * The collection filter entry (EC-246b) — a transient, ad-hoc refine that lives
@@ -34,6 +42,18 @@ export function FilterControl({ collection }) {
34
42
  const filters = source.viewState().filters;
35
43
  const options = filterableFieldOptions(collection);
36
44
  const [open, setOpen] = useState(false);
45
+ // Relation filter values are target ids; read them out as titles on the chip.
46
+ const describeValue = (clause, option) => {
47
+ if (option?.def?.type === 'relation') {
48
+ const target = option.def.target;
49
+ const ids = Array.isArray(clause.value) ? clause.value : [clause.value];
50
+ const titles = ids
51
+ .filter((id) => id !== '' && id != null)
52
+ .map((id) => source.titleOf({ collection: target, id: String(id) }));
53
+ return titles.join(', ');
54
+ }
55
+ return defaultDescribeValue(clause, option);
56
+ };
37
57
  function addFirstClause() {
38
58
  source.setViewState((prev) => prev.filters.length > 0
39
59
  ? prev
@@ -44,7 +64,7 @@ export function FilterControl({ collection }) {
44
64
  source.setViewState((prev) => ({ ...prev, filters: [] }));
45
65
  setOpen(false);
46
66
  }
47
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverAnchor, { asChild: true, children: _jsx("div", { className: "inline-flex items-center", children: filters.length === 0 ? (_jsxs(Button, { variant: "ghost", size: "sm", "data-testid": "filter-entry", onClick: addFirstClause, children: [_jsx(ListFilter, {}), " ", translate('cms.filter.button')] })) : (_jsxs("div", { className: cn('inline-flex items-center gap-0.5 rounded-md border border-dashed border-primary/40', 'bg-primary/5 py-0.5 pl-2 pr-0.5 text-xs'), "data-testid": "filter-chip", children: [_jsx("button", { type: "button", className: "max-w-[220px] truncate text-foreground", onClick: () => setOpen(true), "data-testid": "filter-chip-edit", children: summarizeFilters(filters, options) }), _jsx("button", { type: "button", "aria-label": translate('cms.filter.clearAria'), className: "rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", onClick: clearAll, "data-testid": "filter-chip-clear", children: _jsx(X, { className: "size-3.5" }) })] })) }) }), _jsx(PopoverContent, { align: "start", className: "w-[min(92vw,440px)]", children: _jsxs(Stack, { gap: "2", children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "uppercase tracking-wide", children: translate('cms.filter.refineSession') }), _jsx(Spacer, {}), filters.length > 0 ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: clearAll, "data-testid": "filter-clear-all", children: translate('cms.filter.clearAll') })) : null] }), _jsx(FilterBuilder, { collection: collection })] }) })] }));
67
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverAnchor, { asChild: true, children: _jsx("div", { className: "inline-flex items-center", children: filters.length === 0 ? (_jsxs(Button, { variant: "ghost", size: "sm", "data-testid": "filter-entry", onClick: addFirstClause, children: [_jsx(ListFilter, {}), " ", translate('cms.filter.button')] })) : (_jsxs("div", { className: cn('inline-flex items-center gap-0.5 rounded-md border border-dashed border-primary/40', 'bg-primary/5 py-0.5 pl-2 pr-0.5 text-xs'), "data-testid": "filter-chip", children: [_jsx("button", { type: "button", className: "max-w-[220px] truncate text-foreground", onClick: () => setOpen(true), "data-testid": "filter-chip-edit", children: summarizeFilters(filters, options, describeValue) }), _jsx("button", { type: "button", "aria-label": translate('cms.filter.clearAria'), className: "rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", onClick: clearAll, "data-testid": "filter-chip-clear", children: _jsx(X, { className: "size-3.5" }) })] })) }) }), _jsx(PopoverContent, { align: "start", className: "w-[min(92vw,440px)]", children: _jsxs(Stack, { gap: "2", children: [_jsxs(Row, { gap: "2", align: "center", children: [_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "uppercase tracking-wide", children: translate('cms.filter.refineSession') }), _jsx(Spacer, {}), filters.length > 0 ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: clearAll, "data-testid": "filter-clear-all", children: translate('cms.filter.clearAll') })) : null] }), _jsx(FilterBuilder, { collection: collection })] }) })] }));
48
68
  }
49
69
  /**
50
70
  * The filter builder of the collection table (EC-169). Strictly the closed
@@ -70,27 +90,53 @@ export function FilterBuilder({ collection }) {
70
90
  /** One AND-condition: field · operator · value, plus its validation issue. */
71
91
  function FilterClauseRow({ clause, index, options, issue, onChange, onRemove, }) {
72
92
  const option = options.find((candidate) => candidate.field === clause.field);
73
- const operators = option ? operatorsFor(option) : [clause.op];
93
+ const choices = option ? operatorChoices(option) : [];
94
+ const currentToken = tokenOfClause(clause);
74
95
  function changeField(field) {
75
96
  const next = options.find((candidate) => candidate.field === field);
76
97
  if (next)
77
98
  onChange(defaultClause(next));
78
99
  }
79
- function changeOperator(op) {
80
- if (op === 'in') {
81
- const list = Array.isArray(clause.value) ? clause.value : [clause.value];
82
- onChange({ ...clause, op, value: list.filter((value) => value !== '') });
83
- }
84
- else {
85
- const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value;
86
- onChange({ ...clause, op, value: scalar });
87
- }
100
+ function changeOperator(token) {
101
+ const choice = choices.find((candidate) => candidate.token === token);
102
+ if (!choice)
103
+ return;
104
+ onChange({ ...clause, op: choice.op, value: valueForChoice(choice, clause.value) });
105
+ }
106
+ return (_jsxs(Stack, { gap: "1", "data-testid": `filter-clause-${index}`, children: [_jsxs(Row, { gap: "1", className: "rounded-md border border-border bg-muted/30 p-1", "aria-invalid": issue ? true : undefined, children: [_jsxs(Select, { "aria-label": translate('cms.filter.fieldAria'), value: option ? clause.field : '', onChange: (event) => changeField(event.target.value), className: "h-7 w-auto py-0 text-xs", "data-testid": `filter-field-${index}`, children: [option ? null : (_jsx("option", { value: "", disabled: true, children: clause.field })), options.map((candidate) => (_jsx("option", { value: candidate.field, children: candidate.label }, candidate.field)))] }), _jsx(Select, { "aria-label": translate('cms.filter.operatorAria'), value: currentToken, onChange: (event) => changeOperator(event.target.value), className: "h-7 w-auto py-0 text-xs", "data-testid": `filter-op-${index}`, children: choices.length === 0 ? (_jsx("option", { value: currentToken, disabled: true, children: translate(operatorLabelKey(clause.op, option, clause.value)) })) : (choices.map((choice) => (_jsx("option", { value: choice.token, children: translate(choice.labelKey) }, choice.token)))) }), _jsx(ClauseValueEditor, { option: option, clause: clause, index: index, onChange: onChange }, `${clause.field}:${clause.op}`), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": translate('cms.filter.remove'), "data-testid": `filter-remove-${index}`, onClick: onRemove, children: _jsx(X, {}) })] }), issue ? (_jsx(Text, { size: "xs", className: "text-destructive", "data-testid": `filter-issue-${index}`, children: issue.message })) : null] }));
107
+ }
108
+ /** The `<select>` token for a clause's current operator (empty carries its true/false). */
109
+ function tokenOfClause(clause) {
110
+ if (clause.op === 'empty')
111
+ return clause.value === false ? 'empty:false' : 'empty:true';
112
+ return clause.op;
113
+ }
114
+ /** The value a clause takes when its operator changes to `choice` (EC-345). */
115
+ function valueForChoice(choice, current) {
116
+ if (choice.presetValue !== undefined)
117
+ return choice.presetValue; // empty → true/false
118
+ if (choice.op === 'in') {
119
+ const list = Array.isArray(current) ? current : [current];
120
+ return list.filter((value) => value !== '' && typeof value !== 'boolean');
88
121
  }
89
- return (_jsxs(Stack, { gap: "1", "data-testid": `filter-clause-${index}`, children: [_jsxs(Row, { gap: "1", className: "rounded-md border border-border bg-muted/30 p-1", "aria-invalid": issue ? true : undefined, children: [_jsxs(Select, { "aria-label": translate('cms.filter.fieldAria'), value: option ? clause.field : '', onChange: (event) => changeField(event.target.value), className: "h-7 w-auto py-0 text-xs", "data-testid": `filter-field-${index}`, children: [option ? null : (_jsx("option", { value: "", disabled: true, children: clause.field })), options.map((candidate) => (_jsx("option", { value: candidate.field, children: candidate.label }, candidate.field)))] }), _jsx(Select, { "aria-label": translate('cms.filter.operatorAria'), value: clause.op, onChange: (event) => changeOperator(event.target.value), className: "h-7 w-auto py-0 text-xs", "data-testid": `filter-op-${index}`, children: operators.map((op) => (_jsx("option", { value: op, children: OPERATOR_LABELS[op] }, op))) }), _jsx(ClauseValueEditor, { option: option, clause: clause, index: index, onChange: onChange }, `${clause.field}:${clause.op}`), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": translate('cms.filter.remove'), "data-testid": `filter-remove-${index}`, onClick: onRemove, children: _jsx(X, {}) })] }), issue ? (_jsx(Text, { size: "xs", className: "text-destructive", "data-testid": `filter-issue-${index}`, children: issue.message })) : null] }));
122
+ // A scalar operator: collapse an array to its first entry; drop a boolean left
123
+ // over from the `empty` operator so the reused value control starts clean.
124
+ const scalar = Array.isArray(current) ? (current[0] ?? '') : current;
125
+ return typeof scalar === 'boolean' ? '' : scalar;
90
126
  }
91
127
  /** The typed value input for a clause (closed per field type and operator). */
92
128
  function ClauseValueEditor({ option, clause, index, onChange, }) {
93
129
  const testId = `filter-value-${index}`;
130
+ // `empty` (is empty / is not empty) carries its verdict in the operator choice —
131
+ // there is no value to enter (EC-345).
132
+ if (clause.op === 'empty')
133
+ return null;
134
+ // A relation filter picks its target document(s) via the SAME relation picker the
135
+ // form/inline-cell use — ids are never typed (EC-345). `eq` (incl. membership on a
136
+ // `multiple` relation) picks one; `is any of` (`in`) picks several.
137
+ if (option?.def?.type === 'relation') {
138
+ return (_jsx(RelationClauseValue, { target: option.def.target, clause: clause, testId: testId, onChange: onChange }));
139
+ }
94
140
  if (clause.op === 'in') {
95
141
  return (_jsx(DraftValueInput, { ariaLabel: translate('cms.filter.valueAria'), initial: scalarListToText(clause.value), placeholder: translate('cms.filter.valueListPlaceholder'), testId: testId, onText: (raw) => onChange({ ...clause, value: scalarListFromText(option, raw) }) }));
96
142
  }
@@ -101,4 +147,25 @@ function ClauseValueEditor({ option, clause, index, onChange, }) {
101
147
  // Every concrete scalar field shares the bulk set-field control (EC-247).
102
148
  return (_jsx(ScalarFieldValueEditor, { option: option, value: scalar, ariaLabel: translate('cms.filter.valueAria'), testId: testId, onChange: (value) => onChange({ ...clause, value }) }));
103
149
  }
150
+ /**
151
+ * The relation filter value input (EC-345): a trigger showing the picked target
152
+ * title(s) that opens the existing {@link RelationPicker}. Ids are never typed. For
153
+ * `eq` (a single target, or membership on a `multiple` relation via array-contains,
154
+ * EC-255) it picks ONE; for `is any of` (`in`) it picks several. The stored value is
155
+ * the target id (a string for `eq`, an id array for `in`).
156
+ */
157
+ function RelationClauseValue({ target, clause, testId, onChange, }) {
158
+ const source = useCollectionView();
159
+ const [open, setOpen] = useState(false);
160
+ const multiPick = clause.op === 'in';
161
+ const ids = (Array.isArray(clause.value) ? clause.value : [clause.value])
162
+ .filter((value) => value !== '' && value != null)
163
+ .map((value) => String(value));
164
+ const refs = ids.map((id) => ({ collection: target, id }));
165
+ const titles = ids.map((id) => source.titleOf({ collection: target, id }));
166
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", className: "h-7 max-w-[200px] truncate py-0 text-xs font-normal", "data-testid": testId, onClick: () => setOpen(true), children: titles.length > 0 ? titles.join(', ') : translate('cms.filter.pickRelation') }), _jsx(RelationPicker, { targetCollection: target, multiple: multiPick, value: refs, open: open, onOpenChange: setOpen, onChange: (next) => onChange({
167
+ ...clause,
168
+ value: multiPick ? next.map((ref) => ref.id) : (next[0]?.id ?? ''),
169
+ }) })] }));
170
+ }
104
171
  //# sourceMappingURL=filter-builder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"filter-builder.js","sourceRoot":"","sources":["../../../src/components/cms/filter-builder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { ListFilter, X } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { FilterOperator } from '@elytracms/core/content'\nimport {\n Button,\n Popover,\n PopoverAnchor,\n PopoverContent,\n Row,\n Select,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n OPERATOR_LABELS,\n STATE_FIELD,\n defaultClause,\n filterableFieldOptions,\n operatorsFor,\n scalarListFromText,\n scalarListToText,\n validateClauses,\n} from '@elytracms/studio/lib/cms-views'\nimport type { ClauseIssue, FilterClause, FilterableFieldOption } from '@elytracms/studio/lib/cms-views'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor'\n\n/**\n * A one-line summary of the active filter for the chip face (EC-246b): the\n * single condition read out (\"Status equals news\"), or \"N filters\" for several.\n */\nexport function summarizeFilters(\n filters: readonly FilterClause[],\n options: readonly FilterableFieldOption[],\n): string {\n if (filters.length === 0) return translate('cms.filter.button')\n if (filters.length > 1) return translate('cms.filter.summaryMultiple', { count: filters.length })\n const clause = filters[0]!\n const label = options.find((option) => option.field === clause.field)?.label ?? clause.field\n const value = Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value)\n return `${label} ${OPERATOR_LABELS[clause.op]} ${value}`.trim()\n}\n\n/**\n * The collection filter entry (EC-246b) — a transient, ad-hoc refine that lives\n * beside the view tabs, not inline. With no active filter it's a compact\n * \"Filter\" button; once refining it's a dashed accent **chip** (distinct from\n * the solid view tabs — a filter is a session lens, not a saved one) whose face\n * opens the condition editor in a **popover** (the table stays visible and\n * updates live) and whose ✕ clears it. Shared/saved views are config-as-code.\n */\nexport function FilterControl({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const filters = source.viewState().filters\n const options = filterableFieldOptions(collection)\n const [open, setOpen] = useState(false)\n\n function addFirstClause() {\n source.setViewState((prev) =>\n prev.filters.length > 0\n ? prev\n : { ...prev, filters: [...prev.filters, defaultClause(options[0]!)] },\n )\n setOpen(true)\n }\n\n function clearAll() {\n source.setViewState((prev) => ({ ...prev, filters: [] }))\n setOpen(false)\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {/* Stable anchor so positioning doesn't jump as the trigger morphs button↔chip. */}\n <PopoverAnchor asChild>\n <div className=\"inline-flex items-center\">\n {filters.length === 0 ? (\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"filter-entry\" onClick={addFirstClause}>\n <ListFilter /> {translate('cms.filter.button')}\n </Button>\n ) : (\n <div\n className={cn(\n 'inline-flex items-center gap-0.5 rounded-md border border-dashed border-primary/40',\n 'bg-primary/5 py-0.5 pl-2 pr-0.5 text-xs',\n )}\n data-testid=\"filter-chip\"\n >\n <button\n type=\"button\"\n className=\"max-w-[220px] truncate text-foreground\"\n onClick={() => setOpen(true)}\n data-testid=\"filter-chip-edit\"\n >\n {summarizeFilters(filters, options)}\n </button>\n <button\n type=\"button\"\n aria-label={translate('cms.filter.clearAria')}\n className=\"rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n onClick={clearAll}\n data-testid=\"filter-chip-clear\"\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n )}\n </div>\n </PopoverAnchor>\n <PopoverContent align=\"start\" className=\"w-[min(92vw,440px)]\">\n <Stack gap=\"2\">\n <Row gap=\"2\" align=\"center\">\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('cms.filter.refineSession')}\n </Text>\n <Spacer />\n {filters.length > 0 ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={clearAll}\n data-testid=\"filter-clear-all\"\n >\n {translate('cms.filter.clearAll')}\n </Button>\n ) : null}\n </Row>\n <FilterBuilder collection={collection} />\n </Stack>\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * The filter builder of the collection table (EC-169). Strictly the closed\n * AD-3 vocabulary: it offers ONLY schema-`filterable` fields (plus the\n * built-in publication state), only `eq`/`in`/`lt`/`lte`/`gt`/`gte`, and\n * conditions combine with AND — there is no OR, no nesting, no free-text\n * query input. Clauses that no longer validate (e.g. a saved view whose field\n * lost its `filterable` flag) are flagged inline and excluded from matching.\n * Rendered inside {@link FilterControl}'s popover (EC-246b).\n */\nexport function FilterBuilder({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const state = source.viewState()\n const options = filterableFieldOptions(collection)\n const issues = validateClauses(collection, state.filters)\n\n // Always update through the previous state — render snapshots go stale\n // across rapid successive events (sort clicks, column toggles).\n function updateFilters(update: (filters: FilterClause[]) => FilterClause[]) {\n source.setViewState((prev) => ({ ...prev, filters: update(prev.filters) }))\n }\n\n return (\n <Row gap=\"2\" wrap align=\"start\" data-testid=\"filter-builder\">\n {state.filters.map((clause, index) => (\n <FilterClauseRow\n key={index}\n clause={clause}\n index={index}\n options={options}\n issue={issues.find((issue) => issue.index === index) ?? null}\n onChange={(next) => updateFilters((filters) => filters.map((c, i) => (i === index ? next : c)))}\n onRemove={() => updateFilters((filters) => filters.filter((_, i) => i !== index))}\n />\n ))}\n <Button\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"add-filter\"\n onClick={() => updateFilters((filters) => [...filters, defaultClause(options[0]!)])}\n >\n <ListFilter /> {translate('cms.filter.add')}\n </Button>\n {state.filters.length > 1 ? (\n <Text as=\"span\" size=\"xs\" tone=\"subtle\" className=\"self-center\">\n {translate('cms.filter.andHint')}\n </Text>\n ) : null}\n </Row>\n )\n}\n\n/** One AND-condition: field · operator · value, plus its validation issue. */\nfunction FilterClauseRow({\n clause,\n index,\n options,\n issue,\n onChange,\n onRemove,\n}: {\n clause: FilterClause\n index: number\n options: FilterableFieldOption[]\n issue: ClauseIssue | null\n onChange: (clause: FilterClause) => void\n onRemove: () => void\n}) {\n const option = options.find((candidate) => candidate.field === clause.field)\n const operators = option ? operatorsFor(option) : [clause.op]\n\n function changeField(field: string) {\n const next = options.find((candidate) => candidate.field === field)\n if (next) onChange(defaultClause(next))\n }\n\n function changeOperator(op: FilterOperator) {\n if (op === 'in') {\n const list = Array.isArray(clause.value) ? clause.value : [clause.value]\n onChange({ ...clause, op, value: list.filter((value) => value !== '') })\n } else {\n const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value\n onChange({ ...clause, op, value: scalar })\n }\n }\n\n return (\n <Stack gap=\"1\" data-testid={`filter-clause-${index}`}>\n <Row\n gap=\"1\"\n className=\"rounded-md border border-border bg-muted/30 p-1\"\n aria-invalid={issue ? true : undefined}\n >\n <Select\n aria-label={translate('cms.filter.fieldAria')}\n value={option ? clause.field : ''}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-field-${index}`}\n >\n {option ? null : (\n <option value=\"\" disabled>\n {clause.field}\n </option>\n )}\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Select\n aria-label={translate('cms.filter.operatorAria')}\n value={clause.op}\n onChange={(event) => changeOperator(event.target.value as FilterOperator)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-op-${index}`}\n >\n {operators.map((op) => (\n <option key={op} value={op}>\n {OPERATOR_LABELS[op]}\n </option>\n ))}\n </Select>\n <ClauseValueEditor\n key={`${clause.field}:${clause.op}`}\n option={option}\n clause={clause}\n index={index}\n onChange={onChange}\n />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.filter.remove')}\n data-testid={`filter-remove-${index}`}\n onClick={onRemove}\n >\n <X />\n </Button>\n </Row>\n {issue ? (\n <Text size=\"xs\" className=\"text-destructive\" data-testid={`filter-issue-${index}`}>\n {issue.message}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/** The typed value input for a clause (closed per field type and operator). */\nfunction ClauseValueEditor({\n option,\n clause,\n index,\n onChange,\n}: {\n option: FilterableFieldOption | undefined\n clause: FilterClause\n index: number\n onChange: (clause: FilterClause) => void\n}) {\n const testId = `filter-value-${index}`\n\n if (clause.op === 'in') {\n return (\n <DraftValueInput\n ariaLabel={translate('cms.filter.valueAria')}\n initial={scalarListToText(clause.value)}\n placeholder={translate('cms.filter.valueListPlaceholder')}\n testId={testId}\n onText={(raw) => onChange({ ...clause, value: scalarListFromText(option, raw) })}\n />\n )\n }\n\n const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value\n\n if (clause.field === STATE_FIELD || option?.def === null) {\n return (\n <Select\n aria-label={translate('cms.filter.valueAria')}\n value={String(scalar)}\n onChange={(event) => onChange({ ...clause, value: event.target.value })}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={testId}\n >\n <option value=\"published\">{translate('cms.filter.state.published')}</option>\n <option value=\"draft\">{translate('cms.filter.state.draft')}</option>\n </Select>\n )\n }\n\n // Every concrete scalar field shares the bulk set-field control (EC-247).\n return (\n <ScalarFieldValueEditor\n option={option}\n value={scalar}\n ariaLabel={translate('cms.filter.valueAria')}\n testId={testId}\n onChange={(value) => onChange({ ...clause, value })}\n />\n )\n}\n"]}
1
+ {"version":3,"file":"filter-builder.js","sourceRoot":"","sources":["../../../src/components/cms/filter-builder.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport { ListFilter, X } from 'lucide-react'\nimport type { CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { FilterScalar } from '@elytracms/core/content'\nimport {\n Button,\n Popover,\n PopoverAnchor,\n PopoverContent,\n Row,\n Select,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport {\n STATE_FIELD,\n defaultClause,\n filterableFieldOptions,\n operatorChoices,\n operatorLabelKey,\n scalarListFromText,\n scalarListToText,\n validateClauses,\n} from '@elytracms/studio/lib/cms-views'\nimport type {\n ClauseIssue,\n FilterClause,\n FilterableFieldOption,\n OperatorChoice,\n} from '@elytracms/studio/lib/cms-views'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { RelationPicker } from './relation-picker'\nimport { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor'\n\n/** How a clause's value reads on the chip face — overridable to resolve relation titles. */\ntype DescribeValue = (clause: FilterClause, option: FilterableFieldOption | undefined) => string\n\nconst defaultDescribeValue: DescribeValue = (clause) =>\n Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value)\n\n/**\n * A one-line summary of the active filter for the chip face (EC-246b): the single\n * condition read out (\"Status is news\", \"Slug contains hello\", \"Author is Ada\"), or\n * \"N filters\" for several. Operator labels are type-aware and localized (EC-345);\n * `empty` reads as just \"… is empty\" with no value; `describeValue` lets the caller\n * swap raw scalars (e.g. a relation id) for a human title.\n */\nexport function summarizeFilters(\n filters: readonly FilterClause[],\n options: readonly FilterableFieldOption[],\n describeValue: DescribeValue = defaultDescribeValue,\n): string {\n if (filters.length === 0) return translate('cms.filter.button')\n if (filters.length > 1) return translate('cms.filter.summaryMultiple', { count: filters.length })\n const clause = filters[0]!\n const option = options.find((candidate) => candidate.field === clause.field)\n const label = option?.label ?? clause.field\n const opLabel = translate(operatorLabelKey(clause.op, option, clause.value))\n if (clause.op === 'empty') return `${label} ${opLabel}`.trim()\n return `${label} ${opLabel} ${describeValue(clause, option)}`.trim()\n}\n\n/**\n * The collection filter entry (EC-246b) — a transient, ad-hoc refine that lives\n * beside the view tabs, not inline. With no active filter it's a compact\n * \"Filter\" button; once refining it's a dashed accent **chip** (distinct from\n * the solid view tabs — a filter is a session lens, not a saved one) whose face\n * opens the condition editor in a **popover** (the table stays visible and\n * updates live) and whose ✕ clears it. Shared/saved views are config-as-code.\n */\nexport function FilterControl({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const filters = source.viewState().filters\n const options = filterableFieldOptions(collection)\n const [open, setOpen] = useState(false)\n\n // Relation filter values are target ids; read them out as titles on the chip.\n const describeValue: DescribeValue = (clause, option) => {\n if (option?.def?.type === 'relation') {\n const target = option.def.target\n const ids = Array.isArray(clause.value) ? clause.value : [clause.value]\n const titles = ids\n .filter((id) => id !== '' && id != null)\n .map((id) => source.titleOf({ collection: target, id: String(id) }))\n return titles.join(', ')\n }\n return defaultDescribeValue(clause, option)\n }\n\n function addFirstClause() {\n source.setViewState((prev) =>\n prev.filters.length > 0\n ? prev\n : { ...prev, filters: [...prev.filters, defaultClause(options[0]!)] },\n )\n setOpen(true)\n }\n\n function clearAll() {\n source.setViewState((prev) => ({ ...prev, filters: [] }))\n setOpen(false)\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n {/* Stable anchor so positioning doesn't jump as the trigger morphs button↔chip. */}\n <PopoverAnchor asChild>\n <div className=\"inline-flex items-center\">\n {filters.length === 0 ? (\n <Button variant=\"ghost\" size=\"sm\" data-testid=\"filter-entry\" onClick={addFirstClause}>\n <ListFilter /> {translate('cms.filter.button')}\n </Button>\n ) : (\n <div\n className={cn(\n 'inline-flex items-center gap-0.5 rounded-md border border-dashed border-primary/40',\n 'bg-primary/5 py-0.5 pl-2 pr-0.5 text-xs',\n )}\n data-testid=\"filter-chip\"\n >\n <button\n type=\"button\"\n className=\"max-w-[220px] truncate text-foreground\"\n onClick={() => setOpen(true)}\n data-testid=\"filter-chip-edit\"\n >\n {summarizeFilters(filters, options, describeValue)}\n </button>\n <button\n type=\"button\"\n aria-label={translate('cms.filter.clearAria')}\n className=\"rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n onClick={clearAll}\n data-testid=\"filter-chip-clear\"\n >\n <X className=\"size-3.5\" />\n </button>\n </div>\n )}\n </div>\n </PopoverAnchor>\n <PopoverContent align=\"start\" className=\"w-[min(92vw,440px)]\">\n <Stack gap=\"2\">\n <Row gap=\"2\" align=\"center\">\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {translate('cms.filter.refineSession')}\n </Text>\n <Spacer />\n {filters.length > 0 ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={clearAll}\n data-testid=\"filter-clear-all\"\n >\n {translate('cms.filter.clearAll')}\n </Button>\n ) : null}\n </Row>\n <FilterBuilder collection={collection} />\n </Stack>\n </PopoverContent>\n </Popover>\n )\n}\n\n/**\n * The filter builder of the collection table (EC-169). Strictly the closed\n * AD-3 vocabulary: it offers ONLY schema-`filterable` fields (plus the\n * built-in publication state), only `eq`/`in`/`lt`/`lte`/`gt`/`gte`, and\n * conditions combine with AND — there is no OR, no nesting, no free-text\n * query input. Clauses that no longer validate (e.g. a saved view whose field\n * lost its `filterable` flag) are flagged inline and excluded from matching.\n * Rendered inside {@link FilterControl}'s popover (EC-246b).\n */\nexport function FilterBuilder({ collection }: { collection: CollectionDef }) {\n const source = useCollectionView()\n const state = source.viewState()\n const options = filterableFieldOptions(collection)\n const issues = validateClauses(collection, state.filters)\n\n // Always update through the previous state — render snapshots go stale\n // across rapid successive events (sort clicks, column toggles).\n function updateFilters(update: (filters: FilterClause[]) => FilterClause[]) {\n source.setViewState((prev) => ({ ...prev, filters: update(prev.filters) }))\n }\n\n return (\n <Row gap=\"2\" wrap align=\"start\" data-testid=\"filter-builder\">\n {state.filters.map((clause, index) => (\n <FilterClauseRow\n key={index}\n clause={clause}\n index={index}\n options={options}\n issue={issues.find((issue) => issue.index === index) ?? null}\n onChange={(next) => updateFilters((filters) => filters.map((c, i) => (i === index ? next : c)))}\n onRemove={() => updateFilters((filters) => filters.filter((_, i) => i !== index))}\n />\n ))}\n <Button\n variant=\"ghost\"\n size=\"sm\"\n data-testid=\"add-filter\"\n onClick={() => updateFilters((filters) => [...filters, defaultClause(options[0]!)])}\n >\n <ListFilter /> {translate('cms.filter.add')}\n </Button>\n {state.filters.length > 1 ? (\n <Text as=\"span\" size=\"xs\" tone=\"subtle\" className=\"self-center\">\n {translate('cms.filter.andHint')}\n </Text>\n ) : null}\n </Row>\n )\n}\n\n/** One AND-condition: field · operator · value, plus its validation issue. */\nfunction FilterClauseRow({\n clause,\n index,\n options,\n issue,\n onChange,\n onRemove,\n}: {\n clause: FilterClause\n index: number\n options: FilterableFieldOption[]\n issue: ClauseIssue | null\n onChange: (clause: FilterClause) => void\n onRemove: () => void\n}) {\n const option = options.find((candidate) => candidate.field === clause.field)\n const choices = option ? operatorChoices(option) : []\n const currentToken = tokenOfClause(clause)\n\n function changeField(field: string) {\n const next = options.find((candidate) => candidate.field === field)\n if (next) onChange(defaultClause(next))\n }\n\n function changeOperator(token: string) {\n const choice = choices.find((candidate) => candidate.token === token)\n if (!choice) return\n onChange({ ...clause, op: choice.op, value: valueForChoice(choice, clause.value) })\n }\n\n return (\n <Stack gap=\"1\" data-testid={`filter-clause-${index}`}>\n <Row\n gap=\"1\"\n className=\"rounded-md border border-border bg-muted/30 p-1\"\n aria-invalid={issue ? true : undefined}\n >\n <Select\n aria-label={translate('cms.filter.fieldAria')}\n value={option ? clause.field : ''}\n onChange={(event) => changeField(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-field-${index}`}\n >\n {option ? null : (\n <option value=\"\" disabled>\n {clause.field}\n </option>\n )}\n {options.map((candidate) => (\n <option key={candidate.field} value={candidate.field}>\n {candidate.label}\n </option>\n ))}\n </Select>\n <Select\n aria-label={translate('cms.filter.operatorAria')}\n value={currentToken}\n onChange={(event) => changeOperator(event.target.value)}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={`filter-op-${index}`}\n >\n {choices.length === 0 ? (\n <option value={currentToken} disabled>\n {translate(operatorLabelKey(clause.op, option, clause.value))}\n </option>\n ) : (\n choices.map((choice) => (\n <option key={choice.token} value={choice.token}>\n {translate(choice.labelKey)}\n </option>\n ))\n )}\n </Select>\n <ClauseValueEditor\n key={`${clause.field}:${clause.op}`}\n option={option}\n clause={clause}\n index={index}\n onChange={onChange}\n />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.filter.remove')}\n data-testid={`filter-remove-${index}`}\n onClick={onRemove}\n >\n <X />\n </Button>\n </Row>\n {issue ? (\n <Text size=\"xs\" className=\"text-destructive\" data-testid={`filter-issue-${index}`}>\n {issue.message}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/** The `<select>` token for a clause's current operator (empty carries its true/false). */\nfunction tokenOfClause(clause: FilterClause): string {\n if (clause.op === 'empty') return clause.value === false ? 'empty:false' : 'empty:true'\n return clause.op\n}\n\n/** The value a clause takes when its operator changes to `choice` (EC-345). */\nfunction valueForChoice(\n choice: OperatorChoice,\n current: FilterScalar | FilterScalar[],\n): FilterScalar | FilterScalar[] {\n if (choice.presetValue !== undefined) return choice.presetValue // empty → true/false\n if (choice.op === 'in') {\n const list = Array.isArray(current) ? current : [current]\n return list.filter((value) => value !== '' && typeof value !== 'boolean')\n }\n // A scalar operator: collapse an array to its first entry; drop a boolean left\n // over from the `empty` operator so the reused value control starts clean.\n const scalar = Array.isArray(current) ? (current[0] ?? '') : current\n return typeof scalar === 'boolean' ? '' : scalar\n}\n\n/** The typed value input for a clause (closed per field type and operator). */\nfunction ClauseValueEditor({\n option,\n clause,\n index,\n onChange,\n}: {\n option: FilterableFieldOption | undefined\n clause: FilterClause\n index: number\n onChange: (clause: FilterClause) => void\n}) {\n const testId = `filter-value-${index}`\n\n // `empty` (is empty / is not empty) carries its verdict in the operator choice —\n // there is no value to enter (EC-345).\n if (clause.op === 'empty') return null\n\n // A relation filter picks its target document(s) via the SAME relation picker the\n // form/inline-cell use — ids are never typed (EC-345). `eq` (incl. membership on a\n // `multiple` relation) picks one; `is any of` (`in`) picks several.\n if (option?.def?.type === 'relation') {\n return (\n <RelationClauseValue\n target={option.def.target}\n clause={clause}\n testId={testId}\n onChange={onChange}\n />\n )\n }\n\n if (clause.op === 'in') {\n return (\n <DraftValueInput\n ariaLabel={translate('cms.filter.valueAria')}\n initial={scalarListToText(clause.value)}\n placeholder={translate('cms.filter.valueListPlaceholder')}\n testId={testId}\n onText={(raw) => onChange({ ...clause, value: scalarListFromText(option, raw) })}\n />\n )\n }\n\n const scalar = Array.isArray(clause.value) ? (clause.value[0] ?? '') : clause.value\n\n if (clause.field === STATE_FIELD || option?.def === null) {\n return (\n <Select\n aria-label={translate('cms.filter.valueAria')}\n value={String(scalar)}\n onChange={(event) => onChange({ ...clause, value: event.target.value })}\n className=\"h-7 w-auto py-0 text-xs\"\n data-testid={testId}\n >\n <option value=\"published\">{translate('cms.filter.state.published')}</option>\n <option value=\"draft\">{translate('cms.filter.state.draft')}</option>\n </Select>\n )\n }\n\n // Every concrete scalar field shares the bulk set-field control (EC-247).\n return (\n <ScalarFieldValueEditor\n option={option}\n value={scalar}\n ariaLabel={translate('cms.filter.valueAria')}\n testId={testId}\n onChange={(value) => onChange({ ...clause, value })}\n />\n )\n}\n\n/**\n * The relation filter value input (EC-345): a trigger showing the picked target\n * title(s) that opens the existing {@link RelationPicker}. Ids are never typed. For\n * `eq` (a single target, or membership on a `multiple` relation via array-contains,\n * EC-255) it picks ONE; for `is any of` (`in`) it picks several. The stored value is\n * the target id (a string for `eq`, an id array for `in`).\n */\nfunction RelationClauseValue({\n target,\n clause,\n testId,\n onChange,\n}: {\n target: string\n clause: FilterClause\n testId: string\n onChange: (clause: FilterClause) => void\n}) {\n const source = useCollectionView()\n const [open, setOpen] = useState(false)\n const multiPick = clause.op === 'in'\n const ids = (Array.isArray(clause.value) ? clause.value : [clause.value])\n .filter((value) => value !== '' && value != null)\n .map((value) => String(value))\n const refs: DocumentRef[] = ids.map((id) => ({ collection: target, id }))\n const titles = ids.map((id) => source.titleOf({ collection: target, id }))\n\n return (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"h-7 max-w-[200px] truncate py-0 text-xs font-normal\"\n data-testid={testId}\n onClick={() => setOpen(true)}\n >\n {titles.length > 0 ? titles.join(', ') : translate('cms.filter.pickRelation')}\n </Button>\n <RelationPicker\n targetCollection={target}\n multiple={multiPick}\n value={refs}\n open={open}\n onOpenChange={setOpen}\n onChange={(next) =>\n onChange({\n ...clause,\n value: multiPick ? next.map((ref) => ref.id) : (next[0]?.id ?? ''),\n })\n }\n />\n </>\n )\n}\n"]}
@@ -12,8 +12,17 @@ export interface SectionSurfaceProps {
12
12
  }
13
13
  /**
14
14
  * Resolves a section's deterministic state (brief §8.1, §11) and renders it: the
15
- * working content when `working`, else a consistent state view. A small state
16
- * switcher makes every state reachable for inspection.
15
+ * working content when `working`, else a consistent state view.
16
+ *
17
+ * EC-346: the dev {@link StateSwitcher} panel used to render here, unconditionally,
18
+ * above every section — a debug/reproducibility harness that leaked into
19
+ * production shells (a real studio's Assets header showed a "state · default
20
+ * working empty loading error…" testing strip). It no longer mounts by default.
21
+ * The `?state=` URL contract it exercised stays fully live — `resolveSectionState`
22
+ * below still consumes `forcedState`, so every state remains reachable by URL for
23
+ * tests/agents. `StateSwitcher` itself stays exported and covered (nothing was
24
+ * deleted from the state model) so a future dev-only surface — gated behind
25
+ * `import.meta.env.DEV` or a `defineStudio({ devTools: true })` flag — can remount it.
17
26
  */
18
27
  export declare function SectionSurface({ project, section, forcedState, children }: SectionSurfaceProps): import("react").JSX.Element;
19
28
  /**
@@ -11,13 +11,22 @@ export function parseSectionState(raw) {
11
11
  }
12
12
  /**
13
13
  * Resolves a section's deterministic state (brief §8.1, §11) and renders it: the
14
- * working content when `working`, else a consistent state view. A small state
15
- * switcher makes every state reachable for inspection.
14
+ * working content when `working`, else a consistent state view.
15
+ *
16
+ * EC-346: the dev {@link StateSwitcher} panel used to render here, unconditionally,
17
+ * above every section — a debug/reproducibility harness that leaked into
18
+ * production shells (a real studio's Assets header showed a "state · default
19
+ * working empty loading error…" testing strip). It no longer mounts by default.
20
+ * The `?state=` URL contract it exercised stays fully live — `resolveSectionState`
21
+ * below still consumes `forcedState`, so every state remains reachable by URL for
22
+ * tests/agents. `StateSwitcher` itself stays exported and covered (nothing was
23
+ * deleted from the state model) so a future dev-only surface — gated behind
24
+ * `import.meta.env.DEV` or a `defineStudio({ devTools: true })` flag — can remount it.
16
25
  */
17
26
  export function SectionSurface({ project, section, forcedState, children }) {
18
27
  const availability = availabilityOf(project.settings.sectionAvailability, section);
19
28
  const state = resolveSectionState(availability, forcedState);
20
- return (_jsxs(Stack, { gap: "none", className: "min-h-full", children: [_jsx(StateSwitcher, { projectId: project.id, section: section, current: forcedState }), state === 'working' ? (children) : (_jsx(SectionStateView, { state: state, title: SECTIONS[section].label }))] }));
29
+ return (_jsx(Stack, { gap: "none", className: "min-h-full", children: state === 'working' ? children : _jsx(SectionStateView, { state: state, title: SECTIONS[section].label }) }));
21
30
  }
22
31
  /**
23
32
  * Deterministic state switcher (brief §11). Links to the same section with each
@@ -1 +1 @@
1
- {"version":3,"file":"section-surface.js","sourceRoot":"","sources":["../../src/components/section-surface.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport type { ReactNode } from 'react'\nimport { availabilityOf, SECTIONS, sectionStateSchema } from '@elytracms/core/studio-core'\nimport type { Project, ProjectId, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Code, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { resolveSectionState, SectionStateView } from './section-state'\n\n/** Parse a `?state=` search param into a SectionState (undefined when absent/invalid). */\nexport function parseSectionState(raw: unknown): SectionState | undefined {\n const parsed = sectionStateSchema.safeParse(raw)\n return parsed.success ? parsed.data : undefined\n}\n\nexport interface SectionSurfaceProps {\n project: Project\n section: SectionId\n /** Forced state from `?state=`; overrides availability. */\n forcedState?: SectionState\n /** Working-state content. */\n children?: ReactNode\n}\n\n/**\n * Resolves a section's deterministic state (brief §8.1, §11) and renders it: the\n * working content when `working`, else a consistent state view. A small state\n * switcher makes every state reachable for inspection.\n */\nexport function SectionSurface({ project, section, forcedState, children }: SectionSurfaceProps) {\n const availability = availabilityOf(project.settings.sectionAvailability, section)\n const state = resolveSectionState(availability, forcedState)\n\n return (\n <Stack gap=\"none\" className=\"min-h-full\">\n <StateSwitcher projectId={project.id} section={section} current={forcedState} />\n {state === 'working' ? (\n children\n ) : (\n <SectionStateView state={state} title={SECTIONS[section].label} />\n )}\n </Stack>\n )\n}\n\n/**\n * Deterministic state switcher (brief §11). Links to the same section with each\n * `?state=` value so agents/humans can reproduce every state by URL or click.\n */\nexport function StateSwitcher({\n projectId,\n section,\n current,\n}: {\n projectId: ProjectId\n section: SectionId\n current?: SectionState\n}) {\n const states: SectionState[] = ['working', 'empty', 'loading', 'error', 'unavailable', 'planned']\n const activeKey = current ?? 'default'\n return (\n <Row\n gap=\"1\"\n wrap\n className=\"shrink-0 border-b border-dashed border-border/70 px-4 py-2 sm:px-6\"\n data-testid=\"state-switcher\"\n >\n <Text size=\"xs\" tone=\"subtle\" mono className=\"mr-1\">\n state\n </Text>\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{}}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n activeKey === 'default'\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n default\n </Link>\n {states.map((s) => {\n const active = s === activeKey\n return (\n <Link\n key={s}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{ state: s }}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n active\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {s}\n </Link>\n )\n })}\n <Code tone=\"muted\" className=\"ml-2 hidden sm:inline-flex\">\n {activeKey === 'default' ? 'auto' : `?state=${activeKey}`}\n </Code>\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"section-surface.js","sourceRoot":"","sources":["../../src/components/section-surface.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE } from '@elytracms/studio/router'\nimport type { ReactNode } from 'react'\nimport { availabilityOf, SECTIONS, sectionStateSchema } from '@elytracms/core/studio-core'\nimport type { Project, ProjectId, SectionId, SectionState } from '@elytracms/core/studio-core'\nimport { Code, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { resolveSectionState, SectionStateView } from './section-state'\n\n/** Parse a `?state=` search param into a SectionState (undefined when absent/invalid). */\nexport function parseSectionState(raw: unknown): SectionState | undefined {\n const parsed = sectionStateSchema.safeParse(raw)\n return parsed.success ? parsed.data : undefined\n}\n\nexport interface SectionSurfaceProps {\n project: Project\n section: SectionId\n /** Forced state from `?state=`; overrides availability. */\n forcedState?: SectionState\n /** Working-state content. */\n children?: ReactNode\n}\n\n/**\n * Resolves a section's deterministic state (brief §8.1, §11) and renders it: the\n * working content when `working`, else a consistent state view.\n *\n * EC-346: the dev {@link StateSwitcher} panel used to render here, unconditionally,\n * above every section — a debug/reproducibility harness that leaked into\n * production shells (a real studio's Assets header showed a \"state · default\n * working empty loading error…\" testing strip). It no longer mounts by default.\n * The `?state=` URL contract it exercised stays fully live — `resolveSectionState`\n * below still consumes `forcedState`, so every state remains reachable by URL for\n * tests/agents. `StateSwitcher` itself stays exported and covered (nothing was\n * deleted from the state model) so a future dev-only surface — gated behind\n * `import.meta.env.DEV` or a `defineStudio({ devTools: true })` flag — can remount it.\n */\nexport function SectionSurface({ project, section, forcedState, children }: SectionSurfaceProps) {\n const availability = availabilityOf(project.settings.sectionAvailability, section)\n const state = resolveSectionState(availability, forcedState)\n\n return (\n <Stack gap=\"none\" className=\"min-h-full\">\n {state === 'working' ? children : <SectionStateView state={state} title={SECTIONS[section].label} />}\n </Stack>\n )\n}\n\n/**\n * Deterministic state switcher (brief §11). Links to the same section with each\n * `?state=` value so agents/humans can reproduce every state by URL or click.\n */\nexport function StateSwitcher({\n projectId,\n section,\n current,\n}: {\n projectId: ProjectId\n section: SectionId\n current?: SectionState\n}) {\n const states: SectionState[] = ['working', 'empty', 'loading', 'error', 'unavailable', 'planned']\n const activeKey = current ?? 'default'\n return (\n <Row\n gap=\"1\"\n wrap\n className=\"shrink-0 border-b border-dashed border-border/70 px-4 py-2 sm:px-6\"\n data-testid=\"state-switcher\"\n >\n <Text size=\"xs\" tone=\"subtle\" mono className=\"mr-1\">\n state\n </Text>\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{}}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n activeKey === 'default'\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n default\n </Link>\n {states.map((s) => {\n const active = s === activeKey\n return (\n <Link\n key={s}\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: section }}\n search={{ state: s }}\n className={cn(\n 'rounded-md px-2 py-0.5 font-mono text-xs transition-colors',\n active\n ? 'bg-accent text-accent-foreground'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {s}\n </Link>\n )\n })}\n <Code tone=\"muted\" className=\"ml-2 hidden sm:inline-flex\">\n {activeKey === 'default' ? 'auto' : `?state=${activeKey}`}\n </Code>\n </Row>\n )\n}\n"]}
@@ -21,7 +21,11 @@ export declare const filterScalarSchema: z.ZodUnion<readonly [z.ZodString, z.Zod
21
21
  export declare const filterClauseSchema: z.ZodObject<{
22
22
  field: z.ZodString;
23
23
  op: z.ZodEnum<{
24
+ empty: "empty";
25
+ contains: "contains";
24
26
  in: "in";
27
+ endsWith: "endsWith";
28
+ startsWith: "startsWith";
25
29
  eq: "eq";
26
30
  gt: "gt";
27
31
  gte: "gte";
@@ -43,7 +47,11 @@ export declare const tableViewStateSchema: z.ZodObject<{
43
47
  filters: z.ZodArray<z.ZodObject<{
44
48
  field: z.ZodString;
45
49
  op: z.ZodEnum<{
50
+ empty: "empty";
51
+ contains: "contains";
46
52
  in: "in";
53
+ endsWith: "endsWith";
54
+ startsWith: "startsWith";
47
55
  eq: "eq";
48
56
  gt: "gt";
49
57
  gte: "gte";
@@ -84,14 +92,47 @@ export declare const STATE_OPTION: FilterableFieldOption;
84
92
  */
85
93
  export declare function filterableFieldOptions(collection: CollectionDef): FilterableFieldOption[];
86
94
  export declare function filterOptionFor(collection: CollectionDef, field: string): FilterableFieldOption | undefined;
87
- /** Display order + labels for the closed vocabulary. */
88
- export declare const OPERATOR_LABELS: Record<FilterOperator, string>;
89
95
  /**
90
- * The vocabulary subset that makes sense for a field's type. Always a subset
91
- * of the closed `FILTER_OPERATORS` — never an extension.
96
+ * The per-type operator SET the builder offers (EC-345), always a subset of the
97
+ * closed `FILTER_OPERATORS` — the UI narrows the vocabulary, it never extends it.
98
+ * Ratified sets (Oli, 2026-07-08):
99
+ *
100
+ * - text: contains (default) · equals · starts with · ends with · empty
101
+ * - number: equals · < · ≤ · > · ≥ · empty
102
+ * - date: equals · before · … · after · empty (same ops as number, date labels)
103
+ * - boolean: is
104
+ * - select: is · is any of · empty
105
+ * - relation / asset: is · is any of · empty (relation value = the relation picker;
106
+ * `eq` on a `multiple` relation is membership — array-contains, EC-255 semantics)
107
+ * - state pseudo-field: is · is any of (unchanged — a publication state is never empty)
92
108
  */
93
109
  export declare function operatorsFor(option: FilterableFieldOption): FilterOperator[];
94
- /** A sensible starting clause when a field is added to the builder. */
110
+ /**
111
+ * A visual choice in the operator dropdown (EC-345). Usually one per operator,
112
+ * but `empty` splits into two — "is empty" (`true`) and "is not empty" (`false`) —
113
+ * so an editor never has to reason about a boolean value; the choice carries the
114
+ * preset. `token` is the `<option value>`, `labelKey` a catalog key ({@link operatorLabelKey}).
115
+ */
116
+ export interface OperatorChoice {
117
+ token: string;
118
+ op: FilterOperator;
119
+ /** Value forced when this choice is picked (the `empty` true/false), else undefined. */
120
+ presetValue?: FilterScalar;
121
+ labelKey: string;
122
+ }
123
+ /**
124
+ * The operator dropdown entries for a field (EC-345): {@link operatorsFor} expanded
125
+ * for display, with `empty` shown as the two self-describing choices.
126
+ */
127
+ export declare function operatorChoices(option: FilterableFieldOption): OperatorChoice[];
128
+ /**
129
+ * The catalog key for an operator's label (EC-241/EC-345), keyed by operator AND
130
+ * field type — `eq` reads "equals" on text/number, "on" on a date, "is" on an
131
+ * enum-like field; `lt` reads "less than" on a number, "before" on a date. `empty`
132
+ * additionally depends on its VALUE (is empty / is not empty). Pure → unit-tested.
133
+ */
134
+ export declare function operatorLabelKey(op: FilterOperator, option: FilterableFieldOption | undefined, value?: FilterScalar | readonly FilterScalar[]): string;
135
+ /** A sensible starting clause when a field is added to the builder (EC-345). */
95
136
  export declare function defaultClause(option: FilterableFieldOption): FilterClause;
96
137
  /** Coerce one text input into the scalar a clause compares against. */
97
138
  export declare function scalarFromText(option: FilterableFieldOption | undefined, raw: string): FilterScalar;
@@ -119,7 +160,12 @@ export declare function validateClauses(collection: CollectionDef, clauses: read
119
160
  * predicate; fixtures/tests that don't surface `$state` get the safe default.
120
161
  */
121
162
  export type IsPublished = (doc: CmsDocument) => boolean;
122
- /** Does a document satisfy one clause? Same semantics as the content accessors. */
163
+ /**
164
+ * Does a document satisfy one clause? Identical verdict to the content accessors'
165
+ * delivery/listing path: both reduce to `@elytracms/core/content`'s
166
+ * `matchesFieldValue` over the RAW field value (so `contains`/`empty`/multiple-
167
+ * relation membership all behave the same on the table as on the live site).
168
+ */
123
169
  export declare function clauseMatches(collection: CollectionDef, doc: CmsDocument, localeConfig: LocaleConfig, clause: FilterClause, isPublished?: IsPublished): boolean;
124
170
  /**
125
171
  * Sort documents by one column in the canonical deterministic order
@@ -157,7 +203,11 @@ export declare const configViewSchema: z.ZodObject<{
157
203
  filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
158
204
  field: z.ZodString;
159
205
  op: z.ZodEnum<{
206
+ empty: "empty";
207
+ contains: "contains";
160
208
  in: "in";
209
+ endsWith: "endsWith";
210
+ startsWith: "startsWith";
161
211
  eq: "eq";
162
212
  gt: "gt";
163
213
  gte: "gte";