@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.
- package/dist/components/cms/cell.js +48 -7
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/collection-table.js +9 -3
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/doc-state.d.ts +2 -1
- package/dist/components/cms/doc-state.js +4 -2
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +9 -3
- package/dist/components/cms/filter-builder.js +87 -20
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/section-surface.d.ts +11 -2
- package/dist/components/section-surface.js +12 -3
- package/dist/components/section-surface.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +56 -6
- package/dist/lib/cms-views/view-model.js +111 -23
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +25 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
|
21
|
-
const
|
|
22
|
-
|
|
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
|
|
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(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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.
|
|
16
|
-
*
|
|
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.
|
|
15
|
-
*
|
|
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 (
|
|
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":";AAAA,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAErE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAE1F,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEvE,0FAA0F;AAC1F,MAAM,UAAU,iBAAiB,CAAC,GAAY;IAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAA;IAChD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;AACjD,CAAC;AAWD
|
|
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
|
|
91
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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";
|