@elytracms/studio 0.0.27 → 0.0.28
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/convex/assets.ts +19 -10
- package/convex/authority.ts +60 -0
- package/convex/cliTokens.ts +4 -4
- package/convex/cms.ts +12 -12
- package/convex/content.ts +10 -3
- package/convex/delivery.ts +22 -4
- package/convex/guard.ts +28 -11
- package/convex/members.ts +40 -7
- package/convex/redirects.ts +67 -0
- package/convex/references.ts +11 -11
- package/convex/schema.ts +30 -1
- package/convex/sync.ts +17 -10
- package/convex/users.ts +63 -0
- package/dist/components/app-views/default-modules.d.ts +33 -14
- package/dist/components/app-views/default-modules.js +59 -20
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +32 -0
- package/dist/components/app-views/module.js +16 -0
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.d.ts +15 -1
- package/dist/components/app-views/sidebar.js +10 -0
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/cms/asset-picker.js +4 -4
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell.js +16 -13
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +11 -26
- package/dist/components/cms/cms-store.js +25 -160
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.d.ts +10 -0
- package/dist/components/cms/collection-table.js +72 -72
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +14 -0
- package/dist/components/cms/detail-editor.js +224 -72
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +26 -0
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/editable-cell.js +12 -7
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +2 -0
- package/dist/components/cms/field-control.js +40 -30
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +18 -0
- package/dist/components/cms/filter-builder.js +47 -6
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/import-export.js +5 -3
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/reference-confirm.js +8 -9
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +13 -10
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/use-cell-edit.js +13 -7
- package/dist/components/cms/use-cell-edit.js.map +1 -1
- package/dist/components/cms/use-document-versions.d.ts +16 -0
- package/dist/components/cms/use-document-versions.js +31 -0
- package/dist/components/cms/use-document-versions.js.map +1 -0
- package/dist/components/cms/version-compare-text.d.ts +30 -0
- package/dist/components/cms/version-compare-text.js +113 -0
- package/dist/components/cms/version-compare-text.js.map +1 -0
- package/dist/components/cms/version-compare.d.ts +12 -0
- package/dist/components/cms/version-compare.js +271 -0
- package/dist/components/cms/version-compare.js.map +1 -0
- package/dist/components/cms/versions-pane.d.ts +3 -0
- package/dist/components/cms/versions-pane.js +47 -38
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.d.ts +21 -6
- package/dist/components/cms/view-bar.js +42 -57
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.d.ts +15 -0
- package/dist/components/collection-view/cms-collection-view-source.js +158 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -0
- package/dist/components/collection-view/collection-view-context.d.ts +10 -0
- package/dist/components/collection-view/collection-view-context.js +26 -0
- package/dist/components/collection-view/collection-view-context.js.map +1 -0
- package/dist/components/current-user-menu.js +8 -1
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/redirects/redirects-collection-view-source.d.ts +18 -0
- package/dist/components/redirects/redirects-collection-view-source.js +114 -0
- package/dist/components/redirects/redirects-collection-view-source.js.map +1 -0
- package/dist/components/redirects/redirects-module.d.ts +18 -0
- package/dist/components/redirects/redirects-module.js +43 -0
- package/dist/components/redirects/redirects-module.js.map +1 -0
- package/dist/components/redirects/redirects-schema.d.ts +10 -0
- package/dist/components/redirects/redirects-schema.js +42 -0
- package/dist/components/redirects/redirects-schema.js.map +1 -0
- package/dist/components/redirects/redirects-store.d.ts +46 -0
- package/dist/components/redirects/redirects-store.js +124 -0
- package/dist/components/redirects/redirects-store.js.map +1 -0
- package/dist/components/references/references-panel.js +2 -2
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +14 -5
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/users/user-search.d.ts +8 -0
- package/dist/components/users/user-search.js +13 -0
- package/dist/components/users/user-search.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +28 -0
- package/dist/components/users/users-collection-view-source.js +157 -0
- package/dist/components/users/users-collection-view-source.js.map +1 -0
- package/dist/components/users/users-module.d.ts +23 -0
- package/dist/components/users/users-module.js +61 -0
- package/dist/components/users/users-module.js.map +1 -0
- package/dist/components/users/users-schema.d.ts +19 -0
- package/dist/components/users/users-schema.js +38 -0
- package/dist/components/users/users-schema.js.map +1 -0
- package/dist/components/users/users-store.d.ts +40 -0
- package/dist/components/users/users-store.js +87 -0
- package/dist/components/users/users-store.js.map +1 -0
- package/dist/lib/cms-views/index.d.ts +4 -5
- package/dist/lib/cms-views/index.js +4 -5
- package/dist/lib/cms-views/index.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +48 -1
- package/dist/lib/cms-views/view-model.js +77 -1
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +251 -0
- package/dist/lib/collection-view/collection-view-source.js +10 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +7 -0
- package/dist/lib/operations/session-hydration.js +7 -1
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +5 -1
- package/dist/lib/persistence/convex-adapter.js +148 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +1 -8
- package/dist/lib/workspace/index.js +3 -9
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/match.d.ts +4 -0
- package/dist/router/match.js +8 -0
- package/dist/router/match.js.map +1 -1
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/popover.d.ts +13 -0
- package/dist/ui/popover.js +17 -0
- package/dist/ui/popover.js.map +1 -0
- package/dist/ui/row-item.d.ts +16 -0
- package/dist/ui/row-item.js +18 -0
- package/dist/ui/row-item.js.map +1 -0
- package/package.json +3 -2
|
@@ -1,9 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { ListFilter, X } from 'lucide-react';
|
|
3
|
-
import { Button, Row, Select, Stack, Text } from '@elytracms/studio/ui';
|
|
4
|
+
import { Button, Popover, PopoverAnchor, PopoverContent, Row, Select, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
5
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
4
6
|
import { OPERATOR_LABELS, STATE_FIELD, defaultClause, filterableFieldOptions, operatorsFor, scalarListFromText, scalarListToText, validateClauses, } from '@elytracms/studio/lib/cms-views';
|
|
5
|
-
import {
|
|
7
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
6
8
|
import { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor';
|
|
9
|
+
/**
|
|
10
|
+
* A one-line summary of the active filter for the chip face (EC-246b): the
|
|
11
|
+
* single condition read out ("Status equals news"), or "N filters" for several.
|
|
12
|
+
*/
|
|
13
|
+
export function summarizeFilters(filters, options) {
|
|
14
|
+
if (filters.length === 0)
|
|
15
|
+
return 'Filter';
|
|
16
|
+
if (filters.length > 1)
|
|
17
|
+
return `${filters.length} filters`;
|
|
18
|
+
const clause = filters[0];
|
|
19
|
+
const label = options.find((option) => option.field === clause.field)?.label ?? clause.field;
|
|
20
|
+
const value = Array.isArray(clause.value) ? clause.value.join(', ') : String(clause.value);
|
|
21
|
+
return `${label} ${OPERATOR_LABELS[clause.op]} ${value}`.trim();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The collection filter entry (EC-246b) — a transient, ad-hoc refine that lives
|
|
25
|
+
* beside the view tabs, not inline. With no active filter it's a compact
|
|
26
|
+
* "Filter" button; once refining it's a dashed accent **chip** (distinct from
|
|
27
|
+
* the solid view tabs — a filter is a session lens, not a saved one) whose face
|
|
28
|
+
* opens the condition editor in a **popover** (the table stays visible and
|
|
29
|
+
* updates live) and whose ✕ clears it. Shared/saved views are config-as-code.
|
|
30
|
+
*/
|
|
31
|
+
export function FilterControl({ collection }) {
|
|
32
|
+
const source = useCollectionView();
|
|
33
|
+
const filters = source.viewState().filters;
|
|
34
|
+
const options = filterableFieldOptions(collection);
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
function addFirstClause() {
|
|
37
|
+
source.setViewState((prev) => prev.filters.length > 0
|
|
38
|
+
? prev
|
|
39
|
+
: { ...prev, filters: [...prev.filters, defaultClause(options[0])] });
|
|
40
|
+
setOpen(true);
|
|
41
|
+
}
|
|
42
|
+
function clearAll() {
|
|
43
|
+
source.setViewState((prev) => ({ ...prev, filters: [] }));
|
|
44
|
+
setOpen(false);
|
|
45
|
+
}
|
|
46
|
+
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, {}), " Filter"] })) : (_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": "Clear filter", 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: "Refine \u00B7 this session" }), _jsx(Spacer, {}), filters.length > 0 ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: clearAll, "data-testid": "filter-clear-all", children: "Clear all" })) : null] }), _jsx(FilterBuilder, { collection: collection })] }) })] }));
|
|
47
|
+
}
|
|
7
48
|
/**
|
|
8
49
|
* The filter builder of the collection table (EC-169). Strictly the closed
|
|
9
50
|
* AD-3 vocabulary: it offers ONLY schema-`filterable` fields (plus the
|
|
@@ -11,17 +52,17 @@ import { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor';
|
|
|
11
52
|
* conditions combine with AND — there is no OR, no nesting, no free-text
|
|
12
53
|
* query input. Clauses that no longer validate (e.g. a saved view whose field
|
|
13
54
|
* lost its `filterable` flag) are flagged inline and excluded from matching.
|
|
55
|
+
* Rendered inside {@link FilterControl}'s popover (EC-246b).
|
|
14
56
|
*/
|
|
15
57
|
export function FilterBuilder({ collection }) {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const state = cms.tableStateOf(collectionId);
|
|
58
|
+
const source = useCollectionView();
|
|
59
|
+
const state = source.viewState();
|
|
19
60
|
const options = filterableFieldOptions(collection);
|
|
20
61
|
const issues = validateClauses(collection, state.filters);
|
|
21
62
|
// Always update through the previous state — render snapshots go stale
|
|
22
63
|
// across rapid successive events (sort clicks, column toggles).
|
|
23
64
|
function updateFilters(update) {
|
|
24
|
-
|
|
65
|
+
source.setViewState((prev) => ({ ...prev, filters: update(prev.filters) }));
|
|
25
66
|
}
|
|
26
67
|
return (_jsxs(Row, { gap: "2", wrap: true, align: "start", "data-testid": "filter-builder", children: [state.filters.map((clause, index) => (_jsx(FilterClauseRow, { clause: clause, index: index, options: options, issue: issues.find((issue) => issue.index === index) ?? null, onChange: (next) => updateFilters((filters) => filters.map((c, i) => (i === index ? next : c))), onRemove: () => updateFilters((filters) => filters.filter((_, i) => i !== index)) }, index))), _jsxs(Button, { variant: "ghost", size: "sm", "data-testid": "add-filter", onClick: () => updateFilters((filters) => [...filters, defaultClause(options[0])]), children: [_jsx(ListFilter, {}), " Add filter"] }), state.filters.length > 1 ? (_jsx(Text, { as: "span", size: "xs", tone: "subtle", className: "self-center", children: "all conditions must match (AND)" })) : null] }));
|
|
27
68
|
}
|
|
@@ -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 { ListFilter, X } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport type { FilterOperator } from '@elytracms/core/content'\nimport { Button, Row, Select, Stack, Text } from '@elytracms/studio/ui'\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 { useCms } from './cms-store'\nimport { DraftValueInput, ScalarFieldValueEditor } from './scalar-value-editor'\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 */\nexport function FilterBuilder({ collection }: { collection: CollectionDef }) {\n const cms = useCms()\n const collectionId = collection.id\n const state = cms.tableStateOf(collectionId)\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 cms.setTableState(collectionId, (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 /> Add filter\n </Button>\n {state.filters.length > 1 ? (\n <Text as=\"span\" size=\"xs\" tone=\"subtle\" className=\"self-center\">\n all conditions must match (AND)\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=\"Filter field\"\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=\"Filter operator\"\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=\"Remove filter\"\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=\"Filter value\"\n initial={scalarListToText(clause.value)}\n placeholder=\"value, value, …\"\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=\"Filter value\"\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\">published</option>\n <option value=\"draft\">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=\"Filter value\"\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":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAG5C,OAAO,EACL,MAAM,EACN,OAAO,EACP,aAAa,EACb,cAAc,EACd,GAAG,EACH,MAAM,EACN,MAAM,EACN,KAAK,EACL,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EACL,eAAe,EACf,WAAW,EACX,aAAa,EACb,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,GAChB,MAAM,iCAAiC,CAAA;AAExC,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAE/E;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAgC,EAChC,OAAyC;IAEzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IACzC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,OAAO,CAAC,MAAM,UAAU,CAAA;IAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAE,CAAA;IAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,IAAI,MAAM,CAAC,KAAK,CAAA;IAC5F,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1F,OAAO,GAAG,KAAK,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CAAA;AACjE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,UAAU,EAAiC;IACzE,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,EAAE,CAAC,OAAO,CAAA;IAC1C,MAAM,OAAO,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAA;IAClD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEvC,SAAS,cAAc;QACrB,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;YACrB,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,EAAE,CACxE,CAAA;QACD,OAAO,CAAC,IAAI,CAAC,CAAA;IACf,CAAC;IAED,SAAS,QAAQ;QACf,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;QACzD,OAAO,CAAC,KAAK,CAAC,CAAA;IAChB,CAAC;IAED,OAAO,CACL,MAAC,OAAO,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,aAExC,KAAC,aAAa,IAAC,OAAO,kBACpB,cAAK,SAAS,EAAC,0BAA0B,YACtC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACtB,MAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,iBAAa,cAAc,EAAC,OAAO,EAAE,cAAc,aAClF,KAAC,UAAU,KAAG,eACP,CACV,CAAC,CAAC,CAAC,CACF,eACE,SAAS,EAAE,EAAE,CACX,oFAAoF,EACpF,yCAAyC,CAC1C,iBACW,aAAa,aAEzB,iBACE,IAAI,EAAC,QAAQ,EACb,SAAS,EAAC,wCAAwC,EAClD,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,iBAChB,kBAAkB,YAE7B,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,GAC5B,EACT,iBACE,IAAI,EAAC,QAAQ,gBACF,cAAc,EACzB,SAAS,EAAC,+FAA+F,EACzG,OAAO,EAAE,QAAQ,iBACL,mBAAmB,YAE/B,KAAC,CAAC,IAAC,SAAS,EAAC,UAAU,GAAG,GACnB,IACL,CACP,GACG,GACQ,EAChB,KAAC,cAAc,IAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,qBAAqB,YAC3D,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,KAAK,EAAC,QAAQ,aACzB,KAAC,IAAI,IAAC,EAAE,EAAC,MAAM,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,yBAAyB,2CAEnE,EACP,KAAC,MAAM,KAAG,EACT,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CACpB,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,QAAQ,iBACL,kBAAkB,0BAGvB,CACV,CAAC,CAAC,CAAC,IAAI,IACJ,EACN,KAAC,aAAa,IAAC,UAAU,EAAE,UAAU,GAAI,IACnC,GACO,IACT,CACX,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,UAAU,EAAiC;IACzE,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,CAAA;IAChC,MAAM,OAAO,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;IAEzD,uEAAuE;IACvE,gEAAgE;IAChE,SAAS,aAAa,CAAC,MAAmD;QACxE,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;IAC7E,CAAC;IAED,OAAO,CACL,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,IAAI,QAAC,KAAK,EAAC,OAAO,iBAAa,gBAAgB,aACzD,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CACpC,KAAC,eAAe,IAEd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,IAAI,EAC5D,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAC/F,QAAQ,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,IAN5E,KAAK,CAOV,CACH,CAAC,EACF,MAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,iBACG,YAAY,EACxB,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,aAEnF,KAAC,UAAU,KAAG,mBACP,EACR,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAC1B,KAAC,IAAI,IAAC,EAAE,EAAC,MAAM,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,aAAa,gDAExD,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,SAAS,eAAe,CAAC,EACvB,MAAM,EACN,KAAK,EACL,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,GAQT;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC,CAAA;IAC5E,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAE7D,SAAS,WAAW,CAAC,KAAa;QAChC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,KAAK,KAAK,CAAC,CAAA;QACnE,IAAI,IAAI;YAAE,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;IACzC,CAAC;IAED,SAAS,cAAc,CAAC,EAAkB;QACxC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;YACxE,QAAQ,CAAC,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;QAC1E,CAAC;aAAM,CAAC;YACN,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;YACnF,QAAQ,CAAC,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;QAC5C,CAAC;IACH,CAAC;IAED,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,iBAAc,iBAAiB,KAAK,EAAE,aAClD,MAAC,GAAG,IACF,GAAG,EAAC,GAAG,EACP,SAAS,EAAC,iDAAiD,kBAC7C,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,aAEtC,MAAC,MAAM,kBACM,cAAc,EACzB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EACjC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EACpD,SAAS,EAAC,yBAAyB,iBACtB,gBAAgB,KAAK,EAAE,aAEnC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACf,iBAAQ,KAAK,EAAC,EAAE,EAAC,QAAQ,kBACtB,MAAM,CAAC,KAAK,GACN,CACV,EACA,OAAO,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAC1B,iBAA8B,KAAK,EAAE,SAAS,CAAC,KAAK,YACjD,SAAS,CAAC,KAAK,IADL,SAAS,CAAC,KAAK,CAEnB,CACV,CAAC,IACK,EACT,KAAC,MAAM,kBACM,iBAAiB,EAC5B,KAAK,EAAE,MAAM,CAAC,EAAE,EAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,CAAC,KAAuB,CAAC,EACzE,SAAS,EAAC,yBAAyB,iBACtB,aAAa,KAAK,EAAE,YAEhC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CACrB,iBAAiB,KAAK,EAAE,EAAE,YACvB,eAAe,CAAC,EAAE,CAAC,IADT,EAAE,CAEN,CACV,CAAC,GACK,EACT,KAAC,iBAAiB,IAEhB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,QAAQ,IAJb,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,EAAE,EAAE,CAKnC,EACF,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,gBACH,eAAe,iBACb,iBAAiB,KAAK,EAAE,EACrC,OAAO,EAAE,QAAQ,YAEjB,KAAC,CAAC,KAAG,GACE,IACL,EACL,KAAK,CAAC,CAAC,CAAC,CACP,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,kBAAkB,iBAAc,gBAAgB,KAAK,EAAE,YAC9E,KAAK,CAAC,OAAO,GACT,CACR,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC;AAED,+EAA+E;AAC/E,SAAS,iBAAiB,CAAC,EACzB,MAAM,EACN,MAAM,EACN,KAAK,EACL,QAAQ,GAMT;IACC,MAAM,MAAM,GAAG,gBAAgB,KAAK,EAAE,CAAA;IAEtC,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;QACvB,OAAO,CACL,KAAC,eAAe,IACd,SAAS,EAAC,cAAc,EACxB,OAAO,EAAE,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,EACvC,WAAW,EAAC,sBAAiB,EAC7B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,GAChF,CACH,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;IAEnF,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,IAAI,MAAM,EAAE,GAAG,KAAK,IAAI,EAAE,CAAC;QACzD,OAAO,CACL,MAAC,MAAM,kBACM,cAAc,EACzB,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EACvE,SAAS,EAAC,yBAAyB,iBACtB,MAAM,aAEnB,iBAAQ,KAAK,EAAC,WAAW,0BAAmB,EAC5C,iBAAQ,KAAK,EAAC,OAAO,sBAAe,IAC7B,CACV,CAAA;IACH,CAAC;IAED,0EAA0E;IAC1E,OAAO,CACL,KAAC,sBAAsB,IACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAC,cAAc,EACxB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC,GACnD,CACH,CAAA;AACH,CAAC","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 {\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 'Filter'\n if (filters.length > 1) return `${filters.length} filters`\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 /> Filter\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=\"Clear filter\"\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 Refine · this session\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 Clear all\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 /> Add filter\n </Button>\n {state.filters.length > 1 ? (\n <Text as=\"span\" size=\"xs\" tone=\"subtle\" className=\"self-center\">\n all conditions must match (AND)\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=\"Filter field\"\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=\"Filter operator\"\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=\"Remove filter\"\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=\"Filter value\"\n initial={scalarListToText(clause.value)}\n placeholder=\"value, value, …\"\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=\"Filter value\"\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\">published</option>\n <option value=\"draft\">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=\"Filter value\"\n testId={testId}\n onChange={(value) => onChange({ ...clause, value })}\n />\n )\n}\n"]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Badge, Button, Code, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Stack, Text, } from '@elytracms/studio/ui';
|
|
4
|
-
import {
|
|
4
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
5
5
|
/** Import collection data from JSON with validation + error reporting (brief §8.4, EC-079). */
|
|
6
6
|
export function ImportDialog({ collectionId, open, onOpenChange, }) {
|
|
7
|
-
const
|
|
7
|
+
const source = useCollectionView();
|
|
8
8
|
const [text, setText] = useState('');
|
|
9
9
|
const [report, setReport] = useState(null);
|
|
10
10
|
const [parseError, setParseError] = useState(null);
|
|
@@ -26,7 +26,9 @@ export function ImportDialog({ collectionId, open, onOpenChange, }) {
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
29
|
-
|
|
29
|
+
const outcome = source.imports?.importDocuments(items);
|
|
30
|
+
if (outcome)
|
|
31
|
+
setReport(outcome);
|
|
30
32
|
}
|
|
31
33
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Import documents" }), _jsxs(DialogDescription, { children: ["Paste a JSON array of documents for ", _jsx(Code, { tone: "muted", children: collectionId }), ". Each is validated against the collection schema before import."] })] }), _jsx("textarea", { value: text, onChange: (e) => setText(e.target.value), rows: 8, placeholder: '[{ "id": "post-x", "values": { "slug": "x" }, ... }]', className: "w-full rounded-md border border-input bg-background p-2.5 font-mono text-xs text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none" }), parseError ? (_jsx(Text, { size: "sm", className: "text-destructive", children: parseError })) : null, report ? (_jsxs(Stack, { gap: "2", className: "rounded-md border border-border p-3", children: [_jsxs(Badge, { variant: report.added > 0 ? 'success' : 'outline', children: [report.added, " imported"] }), report.rejected.length > 0 ? (_jsxs(Stack, { gap: "1", children: [_jsxs(Text, { size: "xs", tone: "muted", children: [report.rejected.length, " rejected:"] }), report.rejected.map((r, i) => (_jsxs(Text, { size: "xs", className: "text-destructive", children: [_jsx(Code, { tone: "muted", children: r.id }), " \u2014 ", r.issues[0]?.message] }, `${r.id}-${i}`)))] })) : null] })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: "Close" }), _jsx(Button, { onClick: runImport, disabled: !text.trim(), children: "Import" })] })] }) }));
|
|
32
34
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-export.js","sourceRoot":"","sources":["../../../src/components/cms/import-export.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,EACL,KAAK,EACL,MAAM,EACN,IAAI,EACJ,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"import-export.js","sourceRoot":"","sources":["../../../src/components/cms/import-export.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react'\nimport {\n Badge,\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport type { ImportReport } from './cms-store'\n\n/** Import collection data from JSON with validation + error reporting (brief §8.4, EC-079). */\nexport function ImportDialog({\n collectionId,\n open,\n onOpenChange,\n}: {\n collectionId: string\n open: boolean\n onOpenChange: (open: boolean) => void\n}) {\n const source = useCollectionView()\n const [text, setText] = useState('')\n const [report, setReport] = useState<ImportReport | null>(null)\n const [parseError, setParseError] = useState<string | null>(null)\n\n useEffect(() => {\n if (open) {\n setText('')\n setReport(null)\n setParseError(null)\n }\n }, [open])\n\n function runImport() {\n setParseError(null)\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch {\n setParseError('Input is not valid JSON.')\n return\n }\n const items = Array.isArray(parsed) ? parsed : [parsed]\n const outcome = source.imports?.importDocuments(items)\n if (outcome) setReport(outcome)\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-lg\">\n <DialogHeader>\n <DialogTitle>Import documents</DialogTitle>\n <DialogDescription>\n Paste a JSON array of documents for <Code tone=\"muted\">{collectionId}</Code>. Each is\n validated against the collection schema before import.\n </DialogDescription>\n </DialogHeader>\n\n <textarea\n value={text}\n onChange={(e) => setText(e.target.value)}\n rows={8}\n placeholder='[{ \"id\": \"post-x\", \"values\": { \"slug\": \"x\" }, ... }]'\n className=\"w-full rounded-md border border-input bg-background p-2.5 font-mono text-xs text-foreground shadow-sm focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 outline-none\"\n />\n\n {parseError ? (\n <Text size=\"sm\" className=\"text-destructive\">\n {parseError}\n </Text>\n ) : null}\n\n {report ? (\n <Stack gap=\"2\" className=\"rounded-md border border-border p-3\">\n <Badge variant={report.added > 0 ? 'success' : 'outline'}>{report.added} imported</Badge>\n {report.rejected.length > 0 ? (\n <Stack gap=\"1\">\n <Text size=\"xs\" tone=\"muted\">\n {report.rejected.length} rejected:\n </Text>\n {report.rejected.map((r, i) => (\n <Text key={`${r.id}-${i}`} size=\"xs\" className=\"text-destructive\">\n <Code tone=\"muted\">{r.id}</Code> — {r.issues[0]?.message}\n </Text>\n ))}\n </Stack>\n ) : null}\n </Stack>\n ) : null}\n\n <DialogFooter>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n Close\n </Button>\n <Button onClick={runImport} disabled={!text.trim()}>\n Import\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -5,13 +5,13 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
5
5
|
import { documentKey } from '@elytracms/core/cms-core';
|
|
6
6
|
import { buildReferenceImpact, loadInboundReferenceViews } from '@elytracms/studio/lib/references';
|
|
7
7
|
import { Badge, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Row, Stack, Text, } from '@elytracms/studio/ui';
|
|
8
|
-
import {
|
|
8
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
9
9
|
const ACTION_LABELS = {
|
|
10
10
|
unpublish: 'Unpublish',
|
|
11
11
|
delete: 'Delete',
|
|
12
12
|
};
|
|
13
13
|
export function ReferenceConfirmDialog({ projectId, action, targets, open, busy = false, onOpenChange, onConfirm, }) {
|
|
14
|
-
const
|
|
14
|
+
const source = useCollectionView();
|
|
15
15
|
const [status, setStatus] = useState('loading');
|
|
16
16
|
const [impact, setImpact] = useState(null);
|
|
17
17
|
// A stable identity for the acted-on set, so the effect re-runs exactly
|
|
@@ -27,12 +27,11 @@ export function ReferenceConfirmDialog({ projectId, action, targets, open, busy
|
|
|
27
27
|
try {
|
|
28
28
|
// Queued session mutations (and the persisted baseline) must land
|
|
29
29
|
// before the pre-flight query — the index is maintained on write.
|
|
30
|
-
await
|
|
31
|
-
await cms.flushPersistence();
|
|
30
|
+
await source.flush?.();
|
|
32
31
|
const views = await loadInboundReferenceViews(projectId, targets);
|
|
33
32
|
if (cancelled)
|
|
34
33
|
return;
|
|
35
|
-
setImpact(buildReferenceImpact(
|
|
34
|
+
setImpact(buildReferenceImpact(source.collections, action, views));
|
|
36
35
|
setStatus('ready');
|
|
37
36
|
}
|
|
38
37
|
catch {
|
|
@@ -51,10 +50,10 @@ export function ReferenceConfirmDialog({ projectId, action, targets, open, busy
|
|
|
51
50
|
const subject = useMemo(() => {
|
|
52
51
|
if (targets.length === 1) {
|
|
53
52
|
const ref = targets[0];
|
|
54
|
-
return `“${
|
|
53
|
+
return `“${source.titleOf(ref)}”`;
|
|
55
54
|
}
|
|
56
55
|
return `${targets.length} documents`;
|
|
57
|
-
}, [targets,
|
|
56
|
+
}, [targets, source]);
|
|
58
57
|
const blocked = status === 'ready' && (impact?.totalBlocking ?? 0) > 0;
|
|
59
58
|
const totalBroken = impact?.totalBroken ?? 0;
|
|
60
59
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-testid": "reference-confirm", className: "max-h-[80vh] overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: [actionLabel, " ", subject, "?"] }), _jsx(DialogDescription, { "data-testid": "reference-confirm-summary", children: status === 'loading' ? ('Checking inbound references…') : status === 'unavailable' ? ('Inbound references could not be checked — the reference index is unreachable.') : blocked ? (_jsxs(_Fragment, { children: ["Blocked: ", impact.totalBlocking, ' ', impact.totalBlocking === 1 ? 'strict reference prevents' : 'strict references prevent', ' ', "this. Remove the references below first."] })) : totalBroken === 0 ? (_jsxs(_Fragment, { children: ["Not referenced anywhere.", action === 'delete'
|
|
@@ -75,9 +74,9 @@ function ImpactList({ projectId, impact, onNavigate, }) {
|
|
|
75
74
|
return (_jsx(Stack, { gap: "3", "data-testid": "reference-confirm-impact", children: affected.map((doc) => (_jsx(ImpactGroup, { projectId: projectId, doc: doc, grouped: impact.documents.length > 1, onNavigate: onNavigate }, documentKey(doc.ref)))) }));
|
|
76
75
|
}
|
|
77
76
|
function ImpactGroup({ projectId, doc, grouped, onNavigate, }) {
|
|
78
|
-
const
|
|
77
|
+
const source = useCollectionView();
|
|
79
78
|
const blockingKeys = new Set(doc.blocking.map((entry) => entry.key));
|
|
80
|
-
return (_jsxs(Stack, { gap: "2", "data-testid": `reference-confirm-doc-${doc.ref.id}`, children: [grouped ? (_jsxs(Row, { gap: "2", children: [_jsx(Text, { size: "xs", weight: "semibold", tone: "muted", className: "uppercase tracking-wide", children:
|
|
79
|
+
return (_jsxs(Stack, { gap: "2", "data-testid": `reference-confirm-doc-${doc.ref.id}`, children: [grouped ? (_jsxs(Row, { gap: "2", children: [_jsx(Text, { size: "xs", weight: "semibold", tone: "muted", className: "uppercase tracking-wide", children: source.titleOf(doc.ref) }), _jsx(Badge, { variant: "outline", children: doc.broken.length })] })) : null, doc.broken.map((entry) => (_jsx(ImpactEntryRow, { projectId: projectId, entry: entry, blocking: blockingKeys.has(entry.key), onNavigate: onNavigate }, entry.key)))] }));
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
82
|
* The shared EC-173 impact rows: one deep-linked row per broken inbound
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-confirm.js","sourceRoot":"","sources":["../../../src/components/cms/reference-confirm.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { AlertTriangle, ShieldAlert } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { buildReferenceImpact, loadInboundReferenceViews } from '@elytracms/studio/lib/references'\nimport type {\n DocumentReferenceImpact,\n ReferenceAction,\n ReferenceDeepLink,\n ReferenceEntryView,\n ReferenceImpact,\n} from '@elytracms/studio/lib/references'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useCms } from './cms-store'\n\n/**\n * The EC-173 reference-aware confirmation dialog for unpublish/delete —\n * single documents (detail editor) and bulk selections (EC-169 bulk bar)\n * alike. It inverts the industry default, visibly:\n *\n * 1. *Before* the action it enumerates every inbound reference that will\n * break (published variant for unpublish, both variants for delete), with\n * the EC-172 labels and deep links to the exact field/node.\n * 2. Proceeding is allowed — weak references are the default. Zero references\n * render a plain confirm; breakage renders \"You can … — N places will show\n * a fallback\".\n * 3. Strict opt-in relation fields (`strict: true`) flip the same dialog into\n * a real block: the proceed button is disabled and every blocking referrer\n * is named and linked. The `@elytracms/core/operations` layer enforces the same\n * rule server-side (structured conflict error) — this dialog is the\n * friendly face, not the only guard.\n *\n * Pre-flight data goes through operations reference queries only (EC-155\n * index, flushed); impact semantics live in `lib/references/impact.ts`.\n */\n\ntype ImpactStatus = 'loading' | 'ready' | 'unavailable'\n\nconst ACTION_LABELS: Record<ReferenceAction, string> = {\n unpublish: 'Unpublish',\n delete: 'Delete',\n}\n\nexport function ReferenceConfirmDialog({\n projectId,\n action,\n targets,\n open,\n busy = false,\n onOpenChange,\n onConfirm,\n}: {\n projectId: string\n action: ReferenceAction\n /** The documents being acted on (a stable snapshot taken when opening). */\n targets: readonly DocumentRef[]\n open: boolean\n busy?: boolean\n onOpenChange: (open: boolean) => void\n onConfirm: () => void\n}) {\n const cms = useCms()\n const [status, setStatus] = useState<ImpactStatus>('loading')\n const [impact, setImpact] = useState<ReferenceImpact | null>(null)\n\n // A stable identity for the acted-on set, so the effect re-runs exactly\n // when the selection (not the array instance) changes.\n const targetsKey = targets.map((ref) => documentKey(ref)).join('\\n')\n\n useEffect(() => {\n if (!open || targets.length === 0) return\n let cancelled = false\n setStatus('loading')\n setImpact(null)\n void (async () => {\n try {\n // Queued session mutations (and the persisted baseline) must land\n // before the pre-flight query — the index is maintained on write.\n await cms.ensurePersistedBaseline()\n await cms.flushPersistence()\n const views = await loadInboundReferenceViews(projectId, targets)\n if (cancelled) return\n setImpact(buildReferenceImpact(cms.collections, action, views))\n setStatus('ready')\n } catch {\n // The index being unreachable must not trap the editor: degrade to a\n // plain confirm with an explicit \"could not check\" warning.\n if (!cancelled) setStatus('unavailable')\n }\n })()\n return () => {\n cancelled = true\n }\n // `targetsKey` stands in for `targets` (stable identity for the acted-on set).\n }, [open, action, targetsKey, projectId])\n\n const actionLabel = ACTION_LABELS[action]\n const subject = useMemo(() => {\n if (targets.length === 1) {\n const ref = targets[0]!\n return `“${cms.titleOf(ref)}”`\n }\n return `${targets.length} documents`\n }, [targets, cms])\n\n const blocked = status === 'ready' && (impact?.totalBlocking ?? 0) > 0\n const totalBroken = impact?.totalBroken ?? 0\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent data-testid=\"reference-confirm\" className=\"max-h-[80vh] overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>\n {actionLabel} {subject}?\n </DialogTitle>\n <DialogDescription data-testid=\"reference-confirm-summary\">\n {status === 'loading' ? (\n 'Checking inbound references…'\n ) : status === 'unavailable' ? (\n 'Inbound references could not be checked — the reference index is unreachable.'\n ) : blocked ? (\n <>\n Blocked: {impact!.totalBlocking}{' '}\n {impact!.totalBlocking === 1 ? 'strict reference prevents' : 'strict references prevent'}{' '}\n this. Remove the references below first.\n </>\n ) : totalBroken === 0 ? (\n <>\n Not referenced anywhere.\n {action === 'delete'\n ? ` This permanently deletes ${targets.length === 1 ? 'the document' : `the ${targets.length} selected documents`}.`\n : ''}\n </>\n ) : (\n <>\n You can {action} — {impactSummary(targets.length, totalBroken)} will show a\n fallback until fixed. Each one becomes a visible validation issue.\n </>\n )}\n </DialogDescription>\n </DialogHeader>\n\n {status === 'ready' && impact !== null && totalBroken > 0 ? (\n <ImpactList projectId={projectId} impact={impact} onNavigate={() => onOpenChange(false)} />\n ) : null}\n {status === 'unavailable' ? (\n <Row gap=\"2\" data-testid=\"reference-confirm-unavailable\">\n <AlertTriangle className=\"size-4 shrink-0 text-warning\" />\n <Text size=\"xs\" tone=\"muted\">\n Proceeding is allowed, but affected places cannot be listed right now.\n </Text>\n </Row>\n ) : null}\n\n <DialogFooter>\n <Button\n variant=\"outline\"\n data-testid=\"reference-confirm-cancel\"\n onClick={() => onOpenChange(false)}\n >\n Cancel\n </Button>\n <Button\n variant={action === 'delete' ? 'destructive' : 'default'}\n disabled={status === 'loading' || blocked || busy}\n data-testid=\"reference-confirm-proceed\"\n onClick={onConfirm}\n >\n {totalBroken > 0 && !blocked ? `${actionLabel} anyway` : actionLabel}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction impactSummary(targetCount: number, totalBroken: number): string {\n const places = `${totalBroken} ${totalBroken === 1 ? 'place' : 'places'}`\n // The bulk aggregate: \"N documents, M inbound references will break.\"\n return targetCount > 1 ? `${targetCount} documents are affected; ${places}` : places\n}\n\n/**\n * The enumerated impact: flat for a single target, grouped per target for\n * bulk selections (per-item detail, EC-169 honesty rules).\n */\nfunction ImpactList({\n projectId,\n impact,\n onNavigate,\n}: {\n projectId: string\n impact: ReferenceImpact\n onNavigate: () => void\n}) {\n const affected = impact.documents.filter((doc) => doc.broken.length > 0)\n return (\n <Stack gap=\"3\" data-testid=\"reference-confirm-impact\">\n {affected.map((doc) => (\n <ImpactGroup\n key={documentKey(doc.ref)}\n projectId={projectId}\n doc={doc}\n grouped={impact.documents.length > 1}\n onNavigate={onNavigate}\n />\n ))}\n </Stack>\n )\n}\n\nfunction ImpactGroup({\n projectId,\n doc,\n grouped,\n onNavigate,\n}: {\n projectId: string\n doc: DocumentReferenceImpact\n grouped: boolean\n onNavigate: () => void\n}) {\n const cms = useCms()\n const blockingKeys = new Set(doc.blocking.map((entry) => entry.key))\n return (\n <Stack gap=\"2\" data-testid={`reference-confirm-doc-${doc.ref.id}`}>\n {grouped ? (\n <Row gap=\"2\">\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {cms.titleOf(doc.ref)}\n </Text>\n <Badge variant=\"outline\">{doc.broken.length}</Badge>\n </Row>\n ) : null}\n {doc.broken.map((entry) => (\n <ImpactEntryRow\n key={entry.key}\n projectId={projectId}\n entry={entry}\n blocking={blockingKeys.has(entry.key)}\n onNavigate={onNavigate}\n />\n ))}\n </Stack>\n )\n}\n\n/**\n * The shared EC-173 impact rows: one deep-linked row per broken inbound\n * reference, strict-blocking ones flagged. Rendered by the document\n * confirmation dialog (above) and the asset-delete dialog (EC-202) alike — both\n * reduce to \"these places reference the thing you're removing\", so both lean on\n * the same row + deep-link rendering (which already handles asset link kinds).\n */\nexport function ReferenceImpactRows({\n projectId,\n broken,\n blocking = [],\n onNavigate,\n}: {\n projectId: string\n broken: readonly ReferenceEntryView[]\n blocking?: readonly ReferenceEntryView[]\n onNavigate: () => void\n}) {\n const blockingKeys = new Set(blocking.map((entry) => entry.key))\n return (\n <Stack gap=\"2\" data-testid=\"reference-impact-rows\">\n {broken.map((entry) => (\n <ImpactEntryRow\n key={entry.key}\n projectId={projectId}\n entry={entry}\n blocking={blockingKeys.has(entry.key)}\n onNavigate={onNavigate}\n />\n ))}\n </Stack>\n )\n}\n\n/**\n * One affected place: the EC-172 labels with a working deep link to the exact\n * field/node, the entry kind, draft/published visibility — and a strict\n * marker when this entry is what blocks the action.\n */\nfunction ImpactEntryRow({\n projectId,\n entry,\n blocking,\n onNavigate,\n}: {\n projectId: string\n entry: ReferenceEntryView\n blocking: boolean\n onNavigate: () => void\n}) {\n return (\n <Row\n gap=\"2\"\n className={\n blocking\n ? 'rounded-md border border-destructive/50 bg-destructive/5 px-2.5 py-2'\n : 'rounded-md border border-border/70 bg-background/40 px-2.5 py-2'\n }\n data-testid=\"reference-confirm-entry\"\n >\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <ImpactDeepLink\n projectId={projectId}\n link={entry.sourceLink}\n label={entry.sourceLabel}\n primary\n onNavigate={onNavigate}\n />\n <Row gap=\"1\" className=\"min-w-0\">\n <Text size=\"xs\" tone=\"muted\" className=\"shrink-0\">\n at\n </Text>\n <ImpactDeepLink\n projectId={projectId}\n link={entry.sourceLink}\n label={entry.locationLabel}\n onNavigate={onNavigate}\n />\n </Row>\n </Stack>\n {blocking ? (\n <Badge variant=\"error\" data-testid=\"reference-confirm-strict\">\n <ShieldAlert className=\"size-3\" /> strict\n </Badge>\n ) : null}\n <Badge variant=\"outline\" className=\"shrink-0\">\n {entry.kindLabel}\n </Badge>\n <Row gap=\"1\" className=\"shrink-0\">\n {entry.draft ? <Badge variant=\"warning\">draft</Badge> : null}\n {entry.published ? <Badge variant=\"success\">published</Badge> : null}\n </Row>\n </Row>\n )\n}\n\n/** A deep link to the exact source location; closes the dialog on navigate. */\nfunction ImpactDeepLink({\n projectId,\n link,\n label,\n primary = false,\n onNavigate,\n}: {\n projectId: string\n link: ReferenceDeepLink | null\n label: string\n primary?: boolean\n onNavigate: () => void\n}) {\n const className = primary\n ? 'min-w-0 truncate text-sm font-medium text-foreground hover:text-primary hover:underline'\n : 'min-w-0 truncate text-xs text-muted-foreground hover:text-primary hover:underline'\n if (link === null) {\n return primary ? (\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n )\n }\n switch (link.kind) {\n case 'cms-field':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsDocument(link.collectionId, link.documentId) }}\n search={link.fieldPath !== undefined ? { field: link.fieldPath } : {}}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n case 'builder-node':\n // The builder redirect (EC-187) only reads `?page=`; the page-graph-only\n // `node` has no home in the document editor and is dropped.\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.builder() }}\n search={link.pageId !== undefined ? { page: link.pageId } : {}}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n case 'component':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.component(link.componentId) }}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n case 'asset':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.assets() }}\n search={{ asset: link.assetId }}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reference-confirm.js","sourceRoot":"","sources":["../../../src/components/cms/reference-confirm.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { AlertTriangle, ShieldAlert } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport { buildReferenceImpact, loadInboundReferenceViews } from '@elytracms/studio/lib/references'\nimport type {\n DocumentReferenceImpact,\n ReferenceAction,\n ReferenceDeepLink,\n ReferenceEntryView,\n ReferenceImpact,\n} from '@elytracms/studio/lib/references'\nimport {\n Badge,\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Row,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * The EC-173 reference-aware confirmation dialog for unpublish/delete —\n * single documents (detail editor) and bulk selections (EC-169 bulk bar)\n * alike. It inverts the industry default, visibly:\n *\n * 1. *Before* the action it enumerates every inbound reference that will\n * break (published variant for unpublish, both variants for delete), with\n * the EC-172 labels and deep links to the exact field/node.\n * 2. Proceeding is allowed — weak references are the default. Zero references\n * render a plain confirm; breakage renders \"You can … — N places will show\n * a fallback\".\n * 3. Strict opt-in relation fields (`strict: true`) flip the same dialog into\n * a real block: the proceed button is disabled and every blocking referrer\n * is named and linked. The `@elytracms/core/operations` layer enforces the same\n * rule server-side (structured conflict error) — this dialog is the\n * friendly face, not the only guard.\n *\n * Pre-flight data goes through operations reference queries only (EC-155\n * index, flushed); impact semantics live in `lib/references/impact.ts`.\n */\n\ntype ImpactStatus = 'loading' | 'ready' | 'unavailable'\n\nconst ACTION_LABELS: Record<ReferenceAction, string> = {\n unpublish: 'Unpublish',\n delete: 'Delete',\n}\n\nexport function ReferenceConfirmDialog({\n projectId,\n action,\n targets,\n open,\n busy = false,\n onOpenChange,\n onConfirm,\n}: {\n projectId: string\n action: ReferenceAction\n /** The documents being acted on (a stable snapshot taken when opening). */\n targets: readonly DocumentRef[]\n open: boolean\n busy?: boolean\n onOpenChange: (open: boolean) => void\n onConfirm: () => void\n}) {\n const source = useCollectionView()\n const [status, setStatus] = useState<ImpactStatus>('loading')\n const [impact, setImpact] = useState<ReferenceImpact | null>(null)\n\n // A stable identity for the acted-on set, so the effect re-runs exactly\n // when the selection (not the array instance) changes.\n const targetsKey = targets.map((ref) => documentKey(ref)).join('\\n')\n\n useEffect(() => {\n if (!open || targets.length === 0) return\n let cancelled = false\n setStatus('loading')\n setImpact(null)\n void (async () => {\n try {\n // Queued session mutations (and the persisted baseline) must land\n // before the pre-flight query — the index is maintained on write.\n await source.flush?.()\n const views = await loadInboundReferenceViews(projectId, targets)\n if (cancelled) return\n setImpact(buildReferenceImpact(source.collections, action, views))\n setStatus('ready')\n } catch {\n // The index being unreachable must not trap the editor: degrade to a\n // plain confirm with an explicit \"could not check\" warning.\n if (!cancelled) setStatus('unavailable')\n }\n })()\n return () => {\n cancelled = true\n }\n // `targetsKey` stands in for `targets` (stable identity for the acted-on set).\n }, [open, action, targetsKey, projectId])\n\n const actionLabel = ACTION_LABELS[action]\n const subject = useMemo(() => {\n if (targets.length === 1) {\n const ref = targets[0]!\n return `“${source.titleOf(ref)}”`\n }\n return `${targets.length} documents`\n }, [targets, source])\n\n const blocked = status === 'ready' && (impact?.totalBlocking ?? 0) > 0\n const totalBroken = impact?.totalBroken ?? 0\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent data-testid=\"reference-confirm\" className=\"max-h-[80vh] overflow-y-auto\">\n <DialogHeader>\n <DialogTitle>\n {actionLabel} {subject}?\n </DialogTitle>\n <DialogDescription data-testid=\"reference-confirm-summary\">\n {status === 'loading' ? (\n 'Checking inbound references…'\n ) : status === 'unavailable' ? (\n 'Inbound references could not be checked — the reference index is unreachable.'\n ) : blocked ? (\n <>\n Blocked: {impact!.totalBlocking}{' '}\n {impact!.totalBlocking === 1 ? 'strict reference prevents' : 'strict references prevent'}{' '}\n this. Remove the references below first.\n </>\n ) : totalBroken === 0 ? (\n <>\n Not referenced anywhere.\n {action === 'delete'\n ? ` This permanently deletes ${targets.length === 1 ? 'the document' : `the ${targets.length} selected documents`}.`\n : ''}\n </>\n ) : (\n <>\n You can {action} — {impactSummary(targets.length, totalBroken)} will show a\n fallback until fixed. Each one becomes a visible validation issue.\n </>\n )}\n </DialogDescription>\n </DialogHeader>\n\n {status === 'ready' && impact !== null && totalBroken > 0 ? (\n <ImpactList projectId={projectId} impact={impact} onNavigate={() => onOpenChange(false)} />\n ) : null}\n {status === 'unavailable' ? (\n <Row gap=\"2\" data-testid=\"reference-confirm-unavailable\">\n <AlertTriangle className=\"size-4 shrink-0 text-warning\" />\n <Text size=\"xs\" tone=\"muted\">\n Proceeding is allowed, but affected places cannot be listed right now.\n </Text>\n </Row>\n ) : null}\n\n <DialogFooter>\n <Button\n variant=\"outline\"\n data-testid=\"reference-confirm-cancel\"\n onClick={() => onOpenChange(false)}\n >\n Cancel\n </Button>\n <Button\n variant={action === 'delete' ? 'destructive' : 'default'}\n disabled={status === 'loading' || blocked || busy}\n data-testid=\"reference-confirm-proceed\"\n onClick={onConfirm}\n >\n {totalBroken > 0 && !blocked ? `${actionLabel} anyway` : actionLabel}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction impactSummary(targetCount: number, totalBroken: number): string {\n const places = `${totalBroken} ${totalBroken === 1 ? 'place' : 'places'}`\n // The bulk aggregate: \"N documents, M inbound references will break.\"\n return targetCount > 1 ? `${targetCount} documents are affected; ${places}` : places\n}\n\n/**\n * The enumerated impact: flat for a single target, grouped per target for\n * bulk selections (per-item detail, EC-169 honesty rules).\n */\nfunction ImpactList({\n projectId,\n impact,\n onNavigate,\n}: {\n projectId: string\n impact: ReferenceImpact\n onNavigate: () => void\n}) {\n const affected = impact.documents.filter((doc) => doc.broken.length > 0)\n return (\n <Stack gap=\"3\" data-testid=\"reference-confirm-impact\">\n {affected.map((doc) => (\n <ImpactGroup\n key={documentKey(doc.ref)}\n projectId={projectId}\n doc={doc}\n grouped={impact.documents.length > 1}\n onNavigate={onNavigate}\n />\n ))}\n </Stack>\n )\n}\n\nfunction ImpactGroup({\n projectId,\n doc,\n grouped,\n onNavigate,\n}: {\n projectId: string\n doc: DocumentReferenceImpact\n grouped: boolean\n onNavigate: () => void\n}) {\n const source = useCollectionView()\n const blockingKeys = new Set(doc.blocking.map((entry) => entry.key))\n return (\n <Stack gap=\"2\" data-testid={`reference-confirm-doc-${doc.ref.id}`}>\n {grouped ? (\n <Row gap=\"2\">\n <Text size=\"xs\" weight=\"semibold\" tone=\"muted\" className=\"uppercase tracking-wide\">\n {source.titleOf(doc.ref)}\n </Text>\n <Badge variant=\"outline\">{doc.broken.length}</Badge>\n </Row>\n ) : null}\n {doc.broken.map((entry) => (\n <ImpactEntryRow\n key={entry.key}\n projectId={projectId}\n entry={entry}\n blocking={blockingKeys.has(entry.key)}\n onNavigate={onNavigate}\n />\n ))}\n </Stack>\n )\n}\n\n/**\n * The shared EC-173 impact rows: one deep-linked row per broken inbound\n * reference, strict-blocking ones flagged. Rendered by the document\n * confirmation dialog (above) and the asset-delete dialog (EC-202) alike — both\n * reduce to \"these places reference the thing you're removing\", so both lean on\n * the same row + deep-link rendering (which already handles asset link kinds).\n */\nexport function ReferenceImpactRows({\n projectId,\n broken,\n blocking = [],\n onNavigate,\n}: {\n projectId: string\n broken: readonly ReferenceEntryView[]\n blocking?: readonly ReferenceEntryView[]\n onNavigate: () => void\n}) {\n const blockingKeys = new Set(blocking.map((entry) => entry.key))\n return (\n <Stack gap=\"2\" data-testid=\"reference-impact-rows\">\n {broken.map((entry) => (\n <ImpactEntryRow\n key={entry.key}\n projectId={projectId}\n entry={entry}\n blocking={blockingKeys.has(entry.key)}\n onNavigate={onNavigate}\n />\n ))}\n </Stack>\n )\n}\n\n/**\n * One affected place: the EC-172 labels with a working deep link to the exact\n * field/node, the entry kind, draft/published visibility — and a strict\n * marker when this entry is what blocks the action.\n */\nfunction ImpactEntryRow({\n projectId,\n entry,\n blocking,\n onNavigate,\n}: {\n projectId: string\n entry: ReferenceEntryView\n blocking: boolean\n onNavigate: () => void\n}) {\n return (\n <Row\n gap=\"2\"\n className={\n blocking\n ? 'rounded-md border border-destructive/50 bg-destructive/5 px-2.5 py-2'\n : 'rounded-md border border-border/70 bg-background/40 px-2.5 py-2'\n }\n data-testid=\"reference-confirm-entry\"\n >\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <ImpactDeepLink\n projectId={projectId}\n link={entry.sourceLink}\n label={entry.sourceLabel}\n primary\n onNavigate={onNavigate}\n />\n <Row gap=\"1\" className=\"min-w-0\">\n <Text size=\"xs\" tone=\"muted\" className=\"shrink-0\">\n at\n </Text>\n <ImpactDeepLink\n projectId={projectId}\n link={entry.sourceLink}\n label={entry.locationLabel}\n onNavigate={onNavigate}\n />\n </Row>\n </Stack>\n {blocking ? (\n <Badge variant=\"error\" data-testid=\"reference-confirm-strict\">\n <ShieldAlert className=\"size-3\" /> strict\n </Badge>\n ) : null}\n <Badge variant=\"outline\" className=\"shrink-0\">\n {entry.kindLabel}\n </Badge>\n <Row gap=\"1\" className=\"shrink-0\">\n {entry.draft ? <Badge variant=\"warning\">draft</Badge> : null}\n {entry.published ? <Badge variant=\"success\">published</Badge> : null}\n </Row>\n </Row>\n )\n}\n\n/** A deep link to the exact source location; closes the dialog on navigate. */\nfunction ImpactDeepLink({\n projectId,\n link,\n label,\n primary = false,\n onNavigate,\n}: {\n projectId: string\n link: ReferenceDeepLink | null\n label: string\n primary?: boolean\n onNavigate: () => void\n}) {\n const className = primary\n ? 'min-w-0 truncate text-sm font-medium text-foreground hover:text-primary hover:underline'\n : 'min-w-0 truncate text-xs text-muted-foreground hover:text-primary hover:underline'\n if (link === null) {\n return primary ? (\n <Text size=\"sm\" weight=\"medium\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"min-w-0 truncate\">\n {label}\n </Text>\n )\n }\n switch (link.kind) {\n case 'cms-field':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.cmsDocument(link.collectionId, link.documentId) }}\n search={link.fieldPath !== undefined ? { field: link.fieldPath } : {}}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n case 'builder-node':\n // The builder redirect (EC-187) only reads `?page=`; the page-graph-only\n // `node` has no home in the document editor and is dropped.\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.builder() }}\n search={link.pageId !== undefined ? { page: link.pageId } : {}}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n case 'component':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.component(link.componentId) }}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n case 'asset':\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: studioPath.assets() }}\n search={{ asset: link.assetId }}\n className={className}\n data-testid=\"reference-confirm-link\"\n onClick={onNavigate}\n >\n {label}\n </Link>\n )\n }\n}\n"]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Check, Search } from 'lucide-react';
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Check, Search, X } from 'lucide-react';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { Badge, Button, Code, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Row, Spacer, Text, } from '@elytracms/studio/ui';
|
|
5
5
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
7
7
|
/** Pick related document(s) from a target collection (brief §8.4, EC-074). */
|
|
8
8
|
export function RelationPicker({ targetCollection, cardinality, value, open, onOpenChange, onChange, excludeIds, }) {
|
|
9
|
-
const
|
|
9
|
+
const source = useCollectionView();
|
|
10
10
|
const [query, setQuery] = useState('');
|
|
11
11
|
const [selected, setSelected] = useState(value);
|
|
12
12
|
// Seed selection only on open transitions — `value` is a fresh array each
|
|
@@ -17,12 +17,12 @@ export function RelationPicker({ targetCollection, cardinality, value, open, onO
|
|
|
17
17
|
setQuery('');
|
|
18
18
|
}
|
|
19
19
|
}, [open]);
|
|
20
|
-
const candidates =
|
|
20
|
+
const candidates = source.referenceCandidates(targetCollection).filter((d) => {
|
|
21
21
|
if (excludeIds?.has(d.id))
|
|
22
22
|
return false;
|
|
23
23
|
if (!query.trim())
|
|
24
24
|
return true;
|
|
25
|
-
return
|
|
25
|
+
return source.titleOf({ collection: targetCollection, id: d.id }).toLowerCase().includes(query.toLowerCase());
|
|
26
26
|
});
|
|
27
27
|
const isSelected = (id) => selected.some((r) => r.id === id);
|
|
28
28
|
function toggle(id) {
|
|
@@ -36,12 +36,15 @@ export function RelationPicker({ targetCollection, cardinality, value, open, onO
|
|
|
36
36
|
setSelected((prev) => (isSelected(id) ? prev.filter((r) => r.id !== id) : [...prev, ref]));
|
|
37
37
|
}
|
|
38
38
|
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Pick ", cardinality === 'many' ? 'related' : 'a related', " document"] }), _jsxs(DialogDescription, { children: ["From ", _jsx(Code, { tone: "muted", children: targetCollection }), cardinality === 'many' ? ' · select multiple' : ''] })] }), _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { autoFocus: true, value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search\u2026", className: "pl-8" })] }), _jsx("div", { className: "max-h-72 overflow-auto", children: candidates.length === 0 ? (_jsxs(Text, { size: "sm", tone: "muted", className: "p-3", children: ["No documents in ", targetCollection, "."] })) : (candidates.map((doc) => {
|
|
39
|
-
const title =
|
|
39
|
+
const title = source.titleOf({ collection: targetCollection, id: doc.id });
|
|
40
40
|
const sel = isSelected(doc.id);
|
|
41
41
|
return (_jsxs("button", { type: "button", onClick: () => toggle(doc.id), className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted', sel && 'bg-accent text-accent-foreground'), children: [_jsx("span", { className: "min-w-0 flex-1 truncate font-medium", children: title }), _jsx(Code, { tone: "muted", children: doc.id }), sel ? _jsx(Check, { className: "size-4 text-primary" }) : null] }, doc.id));
|
|
42
|
-
})) }), cardinality === 'many' ? (_jsx(DialogFooter, { children: _jsxs(Row, { gap: "2", className: "w-full", children: [
|
|
43
|
-
onChange(
|
|
42
|
+
})) }), value.length > 0 || cardinality === 'many' ? (_jsx(DialogFooter, { children: _jsxs(Row, { gap: "2", className: "w-full", children: [value.length > 0 ? (_jsxs(Button, { variant: "ghost", className: "text-destructive hover:text-destructive", "data-testid": "relation-remove", onClick: () => {
|
|
43
|
+
onChange([]);
|
|
44
44
|
onOpenChange(false);
|
|
45
|
-
}, children:
|
|
45
|
+
}, children: [_jsx(X, {}), " Remove"] })) : null, _jsx(Spacer, {}), cardinality === 'many' ? (_jsxs(_Fragment, { children: [_jsxs(Badge, { variant: "outline", children: [selected.length, " selected"] }), _jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { onClick: () => {
|
|
46
|
+
onChange(selected);
|
|
47
|
+
onOpenChange(false);
|
|
48
|
+
}, children: "Apply" })] })) : null] }) })) : null] }) }));
|
|
46
49
|
}
|
|
47
50
|
//# sourceMappingURL=relation-picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relation-picker.js","sourceRoot":"","sources":["../../../src/components/cms/relation-picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,EACL,KAAK,EACL,MAAM,EACN,IAAI,EACJ,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,GAAG,EACH,MAAM,EACN,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,EAC7B,gBAAgB,EAChB,WAAW,EACX,KAAK,EACL,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,UAAU,GAcX;IACC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAA;IACpB,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAgB,KAAK,CAAC,CAAA;IAE9D,0EAA0E;IAC1E,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,EAAE,CAAC;YACT,WAAW,CAAC,KAAK,CAAC,CAAA;YAClB,QAAQ,CAAC,EAAE,CAAC,CAAA;QACd,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEV,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAChE,IAAI,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAA;QACvC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAA;QAC9B,OAAO,GAAG,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IAC5G,CAAC,CAAC,CAAA;IAEF,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;IAEpE,SAAS,MAAM,CAAC,EAAU;QACxB,MAAM,GAAG,GAAgB,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAA;QAC7D,IAAI,WAAW,KAAK,KAAK,EAAE,CAAC;YAC1B,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YAClB,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YACf,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAM;QACR,CAAC;QACD,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IAC5F,CAAC;IAED,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,YAC5C,MAAC,aAAa,IAAC,SAAS,EAAC,UAAU,aACjC,MAAC,YAAY,eACX,MAAC,WAAW,wBAAO,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,iBAAwB,EAC3F,MAAC,iBAAiB,wBACX,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,gBAAgB,GAAQ,EAChD,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,IACjC,IACP,EAEf,eAAK,SAAS,EAAC,UAAU,aACvB,KAAC,MAAM,IAAC,SAAS,EAAC,+FAA+F,GAAG,EACpH,KAAC,KAAK,IACJ,SAAS,QACT,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAC,cAAS,EACrB,SAAS,EAAC,MAAM,GAChB,IACE,EAEN,cAAK,SAAS,EAAC,wBAAwB,YACpC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACzB,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,KAAK,iCACzB,gBAAgB,SAC5B,CACR,CAAC,CAAC,CAAC,CACF,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;wBACrB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;wBACvE,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;wBAC9B,OAAO,CACL,kBAEE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAC7B,SAAS,EAAE,EAAE,CACX,0GAA0G,EAC1G,GAAG,IAAI,kCAAkC,CAC1C,aAED,eAAM,SAAS,EAAC,qCAAqC,YAAE,KAAK,GAAQ,EACpE,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,GAAG,CAAC,EAAE,GAAQ,EACjC,GAAG,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,SAAS,EAAC,qBAAqB,GAAG,CAAC,CAAC,CAAC,IAAI,KAVlD,GAAG,CAAC,EAAE,CAWJ,CACV,CAAA;oBACH,CAAC,CAAC,CACH,GACG,EAEL,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,CACxB,KAAC,YAAY,cACX,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,QAAQ,aAC7B,MAAC,KAAK,IAAC,OAAO,EAAC,SAAS,aAAE,QAAQ,CAAC,MAAM,iBAAkB,EAC3D,KAAC,MAAM,KAAG,EACV,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,uBAEjD,EACT,KAAC,MAAM,IACL,OAAO,EAAE,GAAG,EAAE;oCACZ,QAAQ,CAAC,QAAQ,CAAC,CAAA;oCAClB,YAAY,CAAC,KAAK,CAAC,CAAA;gCACrB,CAAC,sBAGM,IACL,GACO,CAChB,CAAC,CAAC,CAAC,IAAI,IACM,GACT,CACV,CAAA;AACH,CAAC","sourcesContent":["import { Check, Search } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Badge,\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useCms } from './cms-store'\n\n/** Pick related document(s) from a target collection (brief §8.4, EC-074). */\nexport function RelationPicker({\n targetCollection,\n cardinality,\n value,\n open,\n onOpenChange,\n onChange,\n excludeIds,\n}: {\n targetCollection: string\n cardinality: 'one' | 'many'\n value: DocumentRef[]\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (refs: DocumentRef[]) => void\n /**\n * Document ids to omit from the candidate list (EC-218): a `parent` self-relation\n * passes the editing document plus its descendants, so a page can never be set\n * as its own ancestor (which would form a cycle).\n */\n excludeIds?: ReadonlySet<string>\n}) {\n const cms = useCms()\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState<DocumentRef[]>(value)\n\n // Seed selection only on open transitions — `value` is a fresh array each\n // parent render, so depending on it would reset in-progress multi-selection.\n useEffect(() => {\n if (open) {\n setSelected(value)\n setQuery('')\n }\n }, [open])\n\n const candidates = cms.documentsOf(targetCollection).filter((d) => {\n if (excludeIds?.has(d.id)) return false\n if (!query.trim()) return true\n return cms.titleOf({ collection: targetCollection, id: d.id }).toLowerCase().includes(query.toLowerCase())\n })\n\n const isSelected = (id: string) => selected.some((r) => r.id === id)\n\n function toggle(id: string) {\n const ref: DocumentRef = { collection: targetCollection, id }\n if (cardinality === 'one') {\n setSelected([ref])\n onChange([ref])\n onOpenChange(false)\n return\n }\n setSelected((prev) => (isSelected(id) ? prev.filter((r) => r.id !== id) : [...prev, ref]))\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Pick {cardinality === 'many' ? 'related' : 'a related'} document</DialogTitle>\n <DialogDescription>\n From <Code tone=\"muted\">{targetCollection}</Code>\n {cardinality === 'many' ? ' · select multiple' : ''}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search…\"\n className=\"pl-8\"\n />\n </div>\n\n <div className=\"max-h-72 overflow-auto\">\n {candidates.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n No documents in {targetCollection}.\n </Text>\n ) : (\n candidates.map((doc) => {\n const title = cms.titleOf({ collection: targetCollection, id: doc.id })\n const sel = isSelected(doc.id)\n return (\n <button\n key={doc.id}\n type=\"button\"\n onClick={() => toggle(doc.id)}\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted',\n sel && 'bg-accent text-accent-foreground',\n )}\n >\n <span className=\"min-w-0 flex-1 truncate font-medium\">{title}</span>\n <Code tone=\"muted\">{doc.id}</Code>\n {sel ? <Check className=\"size-4 text-primary\" /> : null}\n </button>\n )\n })\n )}\n </div>\n\n {cardinality === 'many' ? (\n <DialogFooter>\n <Row gap=\"2\" className=\"w-full\">\n <Badge variant=\"outline\">{selected.length} selected</Badge>\n <Spacer />\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button\n onClick={() => {\n onChange(selected)\n onOpenChange(false)\n }}\n >\n Apply\n </Button>\n </Row>\n </DialogFooter>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"relation-picker.js","sourceRoot":"","sources":["../../../src/components/cms/relation-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Check, Search, X } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Badge,\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/** Pick related document(s) from a target collection (brief §8.4, EC-074). */\nexport function RelationPicker({\n targetCollection,\n cardinality,\n value,\n open,\n onOpenChange,\n onChange,\n excludeIds,\n}: {\n targetCollection: string\n cardinality: 'one' | 'many'\n value: DocumentRef[]\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (refs: DocumentRef[]) => void\n /**\n * Document ids to omit from the candidate list (EC-218): a `parent` self-relation\n * passes the editing document plus its descendants, so a page can never be set\n * as its own ancestor (which would form a cycle).\n */\n excludeIds?: ReadonlySet<string>\n}) {\n const source = useCollectionView()\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState<DocumentRef[]>(value)\n\n // Seed selection only on open transitions — `value` is a fresh array each\n // parent render, so depending on it would reset in-progress multi-selection.\n useEffect(() => {\n if (open) {\n setSelected(value)\n setQuery('')\n }\n }, [open])\n\n const candidates = source.referenceCandidates(targetCollection).filter((d) => {\n if (excludeIds?.has(d.id)) return false\n if (!query.trim()) return true\n return source.titleOf({ collection: targetCollection, id: d.id }).toLowerCase().includes(query.toLowerCase())\n })\n\n const isSelected = (id: string) => selected.some((r) => r.id === id)\n\n function toggle(id: string) {\n const ref: DocumentRef = { collection: targetCollection, id }\n if (cardinality === 'one') {\n setSelected([ref])\n onChange([ref])\n onOpenChange(false)\n return\n }\n setSelected((prev) => (isSelected(id) ? prev.filter((r) => r.id !== id) : [...prev, ref]))\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>Pick {cardinality === 'many' ? 'related' : 'a related'} document</DialogTitle>\n <DialogDescription>\n From <Code tone=\"muted\">{targetCollection}</Code>\n {cardinality === 'many' ? ' · select multiple' : ''}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search…\"\n className=\"pl-8\"\n />\n </div>\n\n <div className=\"max-h-72 overflow-auto\">\n {candidates.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n No documents in {targetCollection}.\n </Text>\n ) : (\n candidates.map((doc) => {\n const title = source.titleOf({ collection: targetCollection, id: doc.id })\n const sel = isSelected(doc.id)\n return (\n <button\n key={doc.id}\n type=\"button\"\n onClick={() => toggle(doc.id)}\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted',\n sel && 'bg-accent text-accent-foreground',\n )}\n >\n <span className=\"min-w-0 flex-1 truncate font-medium\">{title}</span>\n <Code tone=\"muted\">{doc.id}</Code>\n {sel ? <Check className=\"size-4 text-primary\" /> : null}\n </button>\n )\n })\n )}\n </div>\n\n {value.length > 0 || cardinality === 'many' ? (\n <DialogFooter>\n <Row gap=\"2\" className=\"w-full\">\n {/* EC-246c: a way to CLEAR the relation from the picker — the only\n remove path for a cardinality-`one` relation edited inline in the\n list (the form field has its own per-row X). */}\n {value.length > 0 ? (\n <Button\n variant=\"ghost\"\n className=\"text-destructive hover:text-destructive\"\n data-testid=\"relation-remove\"\n onClick={() => {\n onChange([])\n onOpenChange(false)\n }}\n >\n <X /> Remove\n </Button>\n ) : null}\n <Spacer />\n {cardinality === 'many' ? (\n <>\n <Badge variant=\"outline\">{selected.length} selected</Badge>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button\n onClick={() => {\n onChange(selected)\n onOpenChange(false)\n }}\n >\n Apply\n </Button>\n </>\n ) : null}\n </Row>\n </DialogFooter>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from 'react';
|
|
2
2
|
import { resolveField } from '@elytracms/core/cms-core';
|
|
3
3
|
import { cellErrorMessage, cellKey, nextEditableCell } from './cell-edit';
|
|
4
|
-
import {
|
|
4
|
+
import { useCollectionView } from '../collection-view/collection-view-context';
|
|
5
5
|
export function useCellEdit({ collection, docs, editableFields, }) {
|
|
6
|
-
const
|
|
6
|
+
const source = useCollectionView();
|
|
7
7
|
const [activeKey, setActiveKey] = useState(null);
|
|
8
8
|
const [busyKey, setBusyKey] = useState(null);
|
|
9
9
|
const [errors, setErrors] = useState({});
|
|
@@ -77,15 +77,21 @@ export function useCellEdit({ collection, docs, editableFields, }) {
|
|
|
77
77
|
// Same value as displayed → close (and advance) without any operation.
|
|
78
78
|
// Notably: re-committing an unchanged fallback-shown value dispatches
|
|
79
79
|
// nothing — a localized variant is only created by an actual edit.
|
|
80
|
-
const locale =
|
|
81
|
-
const original = resolveField(collection, doc, field.name, locale,
|
|
80
|
+
const locale = source.activeLocale;
|
|
81
|
+
const original = resolveField(collection, doc, field.name, locale, source.localeConfig).value;
|
|
82
82
|
if (sameCellValue(value, original)) {
|
|
83
83
|
settle(coord, next);
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
|
+
// Inline editing is a capability — absent it (a read-only rail) there is
|
|
87
|
+
// nothing to commit; close the editor without dispatching.
|
|
88
|
+
const commitField = source.inlineEditing?.commitField;
|
|
89
|
+
if (!commitField) {
|
|
90
|
+
settle(coord, null);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
86
93
|
setBusyKey(key);
|
|
87
|
-
void
|
|
88
|
-
.commitFieldEdit({ collection, doc, field, locale, value })
|
|
94
|
+
void commitField({ collection, doc, field, locale, value })
|
|
89
95
|
.then((outcome) => {
|
|
90
96
|
if (outcome.ok) {
|
|
91
97
|
clearError(key);
|
|
@@ -100,7 +106,7 @@ export function useCellEdit({ collection, docs, editableFields, }) {
|
|
|
100
106
|
.finally(() => {
|
|
101
107
|
setBusyKey((prev) => (prev === key ? null : prev));
|
|
102
108
|
});
|
|
103
|
-
}, [
|
|
109
|
+
}, [source, collection, clearError, settle]);
|
|
104
110
|
return {
|
|
105
111
|
containerRef,
|
|
106
112
|
activeKey,
|