@object-ui/app-shell 7.0.0 → 7.1.0
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/CHANGELOG.md +281 -0
- package/dist/console/AppContent.js +14 -2
- package/dist/console/ai/AiChatPage.js +11 -7
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +25 -8
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.js +9 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
- package/dist/views/metadata-admin/ResourceListPage.js +8 -16
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/i18n.js +88 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +26 -0
- package/dist/views/metadata-admin/package-scope.js +43 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
- package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -19,6 +19,49 @@ import { fieldsForNodeType, isFieldVisible, getFieldValue, configKeyOf, FLOW_NOD
|
|
|
19
19
|
import { jsonSchemaToFlowFields } from './json-schema-to-fields';
|
|
20
20
|
import { useActionConfigSchemas } from '../previews/useFlowNodePalette';
|
|
21
21
|
import { FlowNodeConfigField } from './FlowNodeConfigField';
|
|
22
|
+
import { ScreenPreview } from '../previews/ScreenPreview';
|
|
23
|
+
/**
|
|
24
|
+
* Mirror a decision node's `conditions` (branches) onto its outgoing sequence
|
|
25
|
+
* edges, in declared order: branch i -> the i-th out-edge. A branch whose
|
|
26
|
+
* expression is `true` marks its edge as the default/else path; an empty
|
|
27
|
+
* expression clears the guard. Fault / back edges are left untouched.
|
|
28
|
+
*
|
|
29
|
+
* This is what lets a decision authored entirely in Studio actually route at
|
|
30
|
+
* runtime: the engine and the simulator branch on `edge.condition`, NOT on
|
|
31
|
+
* `node.config.conditions` (which is only a node-local branch list). Without
|
|
32
|
+
* the mirror, every out-edge stays unconditional and all branches fire.
|
|
33
|
+
*/
|
|
34
|
+
function syncDecisionEdges(decisionId, conditions, edges) {
|
|
35
|
+
const branches = Array.isArray(conditions) ? conditions : [];
|
|
36
|
+
let bi = 0;
|
|
37
|
+
return edges.map((e) => {
|
|
38
|
+
if (e.source !== decisionId || e.type === 'fault' || e.type === 'back')
|
|
39
|
+
return e;
|
|
40
|
+
const branch = branches[bi++];
|
|
41
|
+
if (!branch || typeof branch !== 'object')
|
|
42
|
+
return e;
|
|
43
|
+
const expr = typeof branch.expression === 'string' ? branch.expression.trim() : '';
|
|
44
|
+
const label = typeof branch.label === 'string' ? branch.label.trim() : '';
|
|
45
|
+
const next = { ...e };
|
|
46
|
+
if (label)
|
|
47
|
+
next.label = label;
|
|
48
|
+
else
|
|
49
|
+
delete next.label;
|
|
50
|
+
if (expr && expr !== 'true') {
|
|
51
|
+
next.condition = expr;
|
|
52
|
+
delete next.isDefault;
|
|
53
|
+
}
|
|
54
|
+
else if (expr === 'true') {
|
|
55
|
+
next.isDefault = true;
|
|
56
|
+
delete next.condition;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
delete next.condition;
|
|
60
|
+
delete next.isDefault;
|
|
61
|
+
}
|
|
62
|
+
return next;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
22
65
|
function asConfig(node) {
|
|
23
66
|
const c = node?.config;
|
|
24
67
|
return c && typeof c === 'object' && !Array.isArray(c) ? c : {};
|
|
@@ -65,6 +108,16 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
65
108
|
}, [configSchemas, node?.type]);
|
|
66
109
|
const config = asConfig(node);
|
|
67
110
|
const visibleFields = fields.filter((f) => isFieldVisible(f, node, fields));
|
|
111
|
+
// `{var}` interpolation source for the screen preview — the flow's declared
|
|
112
|
+
// variables and their defaults (the designer has no live run state).
|
|
113
|
+
const screenVars = React.useMemo(() => {
|
|
114
|
+
const decls = Array.isArray(draft.variables) ? draft.variables : [];
|
|
115
|
+
const out = {};
|
|
116
|
+
for (const v of decls)
|
|
117
|
+
if (v && typeof v.name === 'string')
|
|
118
|
+
out[v.name] = v.defaultValue;
|
|
119
|
+
return out;
|
|
120
|
+
}, [draft]);
|
|
68
121
|
// Only fields stored under `config` "own" a config key; spec-structured
|
|
69
122
|
// blocks (waitEventConfig, etc.) and top-level timeoutMs never suppress an
|
|
70
123
|
// Advanced key.
|
|
@@ -100,9 +153,21 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
100
153
|
onPatch({ nodes: spliceArray(nodes, index, { ...node, ...updates }) });
|
|
101
154
|
};
|
|
102
155
|
const hasExtras = extraJson.trim() !== '';
|
|
156
|
+
// Screen nodes (and the `user_task` alias) get a live end-user preview.
|
|
157
|
+
const isScreen = node.type === 'screen' || node.type === 'user_task';
|
|
103
158
|
const setField = (path, value) => {
|
|
104
159
|
const nextNode = setAtPath(node, path, value);
|
|
105
|
-
|
|
160
|
+
const patch = { nodes: spliceArray(nodes, index, nextNode) };
|
|
161
|
+
// Decision branches drive routing — mirror them onto the node's outgoing
|
|
162
|
+
// edges so the engine/simulator can actually branch (they read
|
|
163
|
+
// edge.condition, not node.config.conditions).
|
|
164
|
+
if (node.type === 'decision' && path.length === 2 && path[0] === 'config' && path[1] === 'conditions') {
|
|
165
|
+
const draftEdges = Array.isArray(draft.edges)
|
|
166
|
+
? (draft.edges)
|
|
167
|
+
: [];
|
|
168
|
+
patch.edges = syncDecisionEdges(node.id, value, draftEdges);
|
|
169
|
+
}
|
|
170
|
+
onPatch(patch);
|
|
106
171
|
};
|
|
107
172
|
const commitAdvanced = () => {
|
|
108
173
|
try {
|
|
@@ -133,7 +198,7 @@ export function FlowNodeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
133
198
|
const typeOptions = FLOW_NODE_TYPE_OPTIONS.includes(node.type)
|
|
134
199
|
? [...FLOW_NODE_TYPE_OPTIONS]
|
|
135
200
|
: [...FLOW_NODE_TYPE_OPTIONS, node.type ?? ''].filter(Boolean);
|
|
136
|
-
return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.flowNode.kind', locale), title: node.label || node.id, onClose: onClearSelection, closeLabel: t('engine.inspector.flowNode.close', locale), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.flowNode.remove', locale), onClick: remove, disabled: readOnly }), children: [_jsx(InspectorTextField, { label: t('engine.inspector.flowNode.id', locale), value: node.id, onCommit: (v) => patchNode({ id: v }), disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.label', locale), value: node.label ?? '', onCommit: (v) => patchNode({ label: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: t('engine.inspector.flowNode.type', locale), value: node.type, options: typeOptions.map((v) => ({ value: v, label: v })), onCommit: (v) => patchNode({ type: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.description', locale), value: node.description ?? '', onCommit: (v) => patchNode({ description: v || undefined }), disabled: readOnly }), fields.length === 0 ? (_jsx("p", { className: "pt-1 text-xs italic text-muted-foreground", children: t('engine.inspector.flowNode.noConfig', locale) })) : (visibleFields.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowNode.configuration', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }))), visibleFields.map((field) => (_jsx(FlowNodeConfigField, { field: field, value: getFieldValue(node, field), onCommit: (v) => setField(field.path, v), disabled: readOnly, locale: locale, context: { draft, node } }, field.id))), hasExtras || advReveal ? (_jsxs("details", { className: "group rounded border bg-muted/20", open: advOpen, onToggle: (e) => setAdvOpen(e.target.open), children: [_jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-muted-foreground", children: t('engine.inspector.flowNode.advanced', locale) }), _jsxs("div", { className: "space-y-1 border-t p-2", children: [_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowNode.advancedHint', locale) }), _jsx("textarea", { value: advText, onChange: (e) => setAdvText(e.target.value), onBlur: commitAdvanced, disabled: readOnly, rows: 6, placeholder: "{ }", className: "w-full rounded border bg-background px-2 py-1.5 font-mono text-xs" }), advError && _jsx("div", { className: "text-xs text-destructive", children: advError })] })] })) : (!readOnly && (_jsxs("button", { type: "button", onClick: () => {
|
|
201
|
+
return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.flowNode.kind', locale), title: node.label || node.id, onClose: onClearSelection, closeLabel: t('engine.inspector.flowNode.close', locale), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.flowNode.remove', locale), onClick: remove, disabled: readOnly }), children: [_jsx(InspectorTextField, { label: t('engine.inspector.flowNode.id', locale), value: node.id, onCommit: (v) => patchNode({ id: v }), disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.label', locale), value: node.label ?? '', onCommit: (v) => patchNode({ label: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: t('engine.inspector.flowNode.type', locale), value: node.type, options: typeOptions.map((v) => ({ value: v, label: v })), onCommit: (v) => patchNode({ type: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: t('engine.inspector.flowNode.description', locale), value: node.description ?? '', onCommit: (v) => patchNode({ description: v || undefined }), disabled: readOnly }), fields.length === 0 ? (_jsx("p", { className: "pt-1 text-xs italic text-muted-foreground", children: t('engine.inspector.flowNode.noConfig', locale) })) : (visibleFields.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 pt-1", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.flowNode.configuration', locale) }), _jsx("span", { className: "h-px flex-1 bg-border", "aria-hidden": true })] }))), visibleFields.map((field) => (_jsx(FlowNodeConfigField, { field: field, value: getFieldValue(node, field), onCommit: (v) => setField(field.path, v), disabled: readOnly, locale: locale, context: { draft, node } }, field.id))), isScreen && _jsx(ScreenPreview, { node: node, variables: screenVars, className: "mt-1" }), hasExtras || advReveal ? (_jsxs("details", { className: "group rounded border bg-muted/20", open: advOpen, onToggle: (e) => setAdvOpen(e.target.open), children: [_jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-muted-foreground", children: t('engine.inspector.flowNode.advanced', locale) }), _jsxs("div", { className: "space-y-1 border-t p-2", children: [_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t('engine.inspector.flowNode.advancedHint', locale) }), _jsx("textarea", { value: advText, onChange: (e) => setAdvText(e.target.value), onBlur: commitAdvanced, disabled: readOnly, rows: 6, placeholder: "{ }", className: "w-full rounded border bg-background px-2 py-1.5 font-mono text-xs" }), advError && _jsx("div", { className: "text-xs text-destructive", children: advError })] })] })) : (!readOnly && (_jsxs("button", { type: "button", onClick: () => {
|
|
137
202
|
setAdvReveal(true);
|
|
138
203
|
setAdvOpen(true);
|
|
139
204
|
}, className: "inline-flex items-center gap-1 self-start text-[11px] text-muted-foreground transition-colors hover:text-foreground", children: [_jsx(Plus, { className: "h-3 w-3" }), t('engine.inspector.flowNode.advanced', locale)] })))] }));
|
|
@@ -21,7 +21,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
21
21
|
import * as React from 'react';
|
|
22
22
|
import { InspectorShell, InspectorTextField } from './_shared';
|
|
23
23
|
import { Label } from '@object-ui/components';
|
|
24
|
-
import {
|
|
24
|
+
import { toFieldNameLoose } from '../previews/object-fields-io';
|
|
25
|
+
import { slugify } from '../createDerive';
|
|
25
26
|
import { t } from '../i18n';
|
|
26
27
|
export function ObjectDefaultInspector({ name, draft, onPatch, readOnly, locale, }) {
|
|
27
28
|
const tr = React.useCallback((key) => t(key, locale), [locale]);
|
|
@@ -33,16 +34,16 @@ export function ObjectDefaultInspector({ name, draft, onPatch, readOnly, locale,
|
|
|
33
34
|
const setLabel = (v) => {
|
|
34
35
|
const patch = { label: v || undefined };
|
|
35
36
|
if (createMode && !nameTouched.current)
|
|
36
|
-
patch.name =
|
|
37
|
+
patch.name = slugify(v);
|
|
37
38
|
onPatch(patch);
|
|
38
39
|
};
|
|
39
40
|
const setName = (v) => {
|
|
40
41
|
nameTouched.current = true;
|
|
41
|
-
onPatch({ name:
|
|
42
|
+
onPatch({ name: toFieldNameLoose(v) });
|
|
42
43
|
};
|
|
43
44
|
const nameValue = createMode ? str('name') : name || str('name');
|
|
44
45
|
const title = str('label') || name || tr('designer.object.kind');
|
|
45
|
-
return (_jsx(InspectorShell, { kindLabel: tr('designer.object.kind'), title: title, onClose: () => { }, hideClose: true, children: _jsxs(Section, { title: tr('designer.object.section.basic'), hint: tr('designer.object.section.basicHint'), children: [_jsx(Field, { hint: tr('designer.object.nameHint'), children: _jsx(InspectorTextField, { label: tr('designer.object.name'), value: nameValue, onCommit: setName, disabled: readOnly || !createMode, mono: true, placeholder: tr('designer.object.namePlaceholder') }) }), _jsx(InspectorTextField, { label: tr('designer.object.label'), value: str('label'), onCommit: setLabel, disabled: readOnly, placeholder: tr('designer.object.labelPlaceholder') }), _jsx(InspectorTextField, { label: tr('designer.object.pluralLabel'), value: str('pluralLabel'), onCommit: (v) => onPatch({ pluralLabel: v || undefined }), disabled: readOnly, placeholder: tr('designer.object.pluralPlaceholder') }), _jsx(Field, { hint: tr('designer.object.iconHint'), children: _jsx(InspectorTextField, { label: tr('designer.object.icon'), value: str('icon'), onCommit: (v) => onPatch({ icon: v || undefined }), disabled: readOnly, mono: true, placeholder: "building" }) }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: tr('designer.object.description') }), _jsx("textarea", { value: str('description'), disabled: readOnly, rows: 2, placeholder: tr('designer.object.descriptionPlaceholder'), onChange: (e) => onPatch({ description: e.target.value || undefined }), className: "w-full text-sm rounded-md border bg-background px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary" })] })] }) }));
|
|
46
|
+
return (_jsx(InspectorShell, { kindLabel: tr('designer.object.kind'), title: title, onClose: () => { }, hideClose: true, children: _jsxs(Section, { title: tr('designer.object.section.basic'), hint: tr('designer.object.section.basicHint'), children: [_jsx(Field, { hint: tr('designer.object.nameHint'), children: _jsx(InspectorTextField, { label: tr('designer.object.name'), value: nameValue, onCommit: setName, disabled: readOnly || !createMode, mono: true, testId: "object-name-input", placeholder: tr('designer.object.namePlaceholder') }) }), _jsx(InspectorTextField, { label: tr('designer.object.label'), value: str('label'), onCommit: setLabel, disabled: readOnly, placeholder: tr('designer.object.labelPlaceholder') }), _jsx(InspectorTextField, { label: tr('designer.object.pluralLabel'), value: str('pluralLabel'), onCommit: (v) => onPatch({ pluralLabel: v || undefined }), disabled: readOnly, placeholder: tr('designer.object.pluralPlaceholder') }), _jsx(Field, { hint: tr('designer.object.iconHint'), children: _jsx(InspectorTextField, { label: tr('designer.object.icon'), value: str('icon'), onCommit: (v) => onPatch({ icon: v || undefined }), disabled: readOnly, mono: true, placeholder: "building" }) }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: tr('designer.object.description') }), _jsx("textarea", { value: str('description'), disabled: readOnly, rows: 2, placeholder: tr('designer.object.descriptionPlaceholder'), onChange: (e) => onPatch({ description: e.target.value || undefined }), className: "w-full text-sm rounded-md border bg-background px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary" })] })] }) }));
|
|
46
47
|
}
|
|
47
48
|
/* ─────────────── Sub-components ─────────────── */
|
|
48
49
|
function Section({ title, hint, children, }) {
|
|
@@ -21,13 +21,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
21
21
|
* auto-rewritten — callers should re-validate downstream.
|
|
22
22
|
*/
|
|
23
23
|
import * as React from 'react';
|
|
24
|
+
import { slugify } from '../createDerive';
|
|
24
25
|
import { useMetadataClient } from '../useMetadata';
|
|
25
26
|
import { InspectorShell, InspectorReorderButtons, InspectorTextField, InspectorNumberField, InspectorSelectField, InspectorCheckboxField, InspectorRemoveButton, InspectorEmptyState, moveArray, } from './_shared';
|
|
26
27
|
import { Button, Input, Label, Badge } from '@object-ui/components';
|
|
27
28
|
import { Plus, X, ArrowUp, ArrowDown, Copy } from 'lucide-react';
|
|
28
29
|
import { InspectorComboField } from './InspectorComboField';
|
|
29
30
|
import { useObjectFields } from '../previews/useObjectFields';
|
|
30
|
-
import { readFields, writeFields,
|
|
31
|
+
import { readFields, writeFields, toFieldNameLoose, indexOfField, } from '../previews/object-fields-io';
|
|
31
32
|
import { FIELD_TYPE_META, TYPES_BY_CATEGORY, CATEGORY_LABEL_EN, CATEGORY_LABEL_ZH, } from '../previews/field-types';
|
|
32
33
|
import { t, tFormat } from '../i18n';
|
|
33
34
|
const isZh = (locale) => (locale ?? '').toLowerCase().startsWith('zh');
|
|
@@ -116,7 +117,7 @@ export function ObjectFieldInspector({ selection, draft, onPatch, onClearSelecti
|
|
|
116
117
|
writeView({ shape: view.shape, entries: nextEntries });
|
|
117
118
|
};
|
|
118
119
|
const setKey = (rawNext) => {
|
|
119
|
-
const nextName =
|
|
120
|
+
const nextName = toFieldNameLoose(rawNext);
|
|
120
121
|
if (!nextName || nextName === entry.name)
|
|
121
122
|
return;
|
|
122
123
|
// Disallow collision
|
|
@@ -127,6 +128,29 @@ export function ObjectFieldInspector({ selection, draft, onPatch, onClearSelecti
|
|
|
127
128
|
writeView({ shape: view.shape, entries: nextEntries });
|
|
128
129
|
onSelectionChange?.({ kind: 'field', id: nextName, label: String(def.label ?? nextName) });
|
|
129
130
|
};
|
|
131
|
+
// Derive the API name from the label (on blur, so we use the complete
|
|
132
|
+
// string — not per keystroke, which would churn the field key) while the
|
|
133
|
+
// name is still an auto-generated default and the user hasn't customised it.
|
|
134
|
+
// Mirrors the object Name behaviour; slugify() returns '' for non-Latin
|
|
135
|
+
// labels, in which case the unique default name is kept.
|
|
136
|
+
const maybeDeriveName = (label) => {
|
|
137
|
+
if (readOnly)
|
|
138
|
+
return;
|
|
139
|
+
const base = type === 'select' ? 'status' : type;
|
|
140
|
+
const isAutoName = entry.name === base ||
|
|
141
|
+
(entry.name.startsWith(`${base}_`) && /^\d+$/.test(entry.name.slice(base.length + 1)));
|
|
142
|
+
if (!isAutoName)
|
|
143
|
+
return;
|
|
144
|
+
const derived = slugify(label);
|
|
145
|
+
if (!derived || derived === entry.name)
|
|
146
|
+
return;
|
|
147
|
+
if (view.entries.some((e, i) => i !== idx && e.name === derived))
|
|
148
|
+
return;
|
|
149
|
+
const nextEntries = [...view.entries];
|
|
150
|
+
nextEntries[idx] = { ...entry, name: derived };
|
|
151
|
+
writeView({ shape: view.shape, entries: nextEntries });
|
|
152
|
+
onSelectionChange?.({ kind: 'field', id: derived, label: String(def.label ?? derived) });
|
|
153
|
+
};
|
|
130
154
|
const removeField = () => {
|
|
131
155
|
const nextEntries = view.entries.filter((_, i) => i !== idx);
|
|
132
156
|
writeView({ shape: view.shape, entries: nextEntries });
|
|
@@ -177,7 +201,7 @@ export function ObjectFieldInspector({ selection, draft, onPatch, onClearSelecti
|
|
|
177
201
|
label: typeof def.label === 'string' ? def.label : entry.name,
|
|
178
202
|
}), onClick: removeField, disabled: readOnly }));
|
|
179
203
|
const typeMetaLabel = isZh(locale) ? typeMeta?.labelZh : typeMeta?.label;
|
|
180
|
-
return (_jsxs(InspectorShell, { kindLabel: tr('designer.field.kind'), title: typeof def.label === 'string' && def.label ? def.label : entry.name, onClose: onClearSelection, closeLabel: tr('designer.field.close'), headerActions: headerActions, footer: footer, children: [_jsxs(Section, { title: tr('designer.field.section.basic'), children: [_jsx(InspectorTextField, { label: tr('designer.field.apiName'), value: entry.name, onCommit: setKey, disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: tr('designer.field.label'), value: typeof def.label === 'string' ? def.label : '', onCommit: (v) => patchDef({ label: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: tr('designer.field.type'), value: type, options: typeOptions, onCommit: (v) => patchDef({ type: v }), disabled: readOnly }), _jsxs("div", { className: "flex items-center gap-4 pt-1", children: [_jsx(InspectorCheckboxField, { label: tr('designer.field.required'), value: !!def.required, onCommit: (v) => patchDef({ required: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.unique'), value: !!def.unique, onCommit: (v) => patchDef({ unique: v || undefined }), disabled: readOnly })] }), _jsx(TextareaField, { label: tr('designer.field.description'), value: typeof def.description === 'string' ? def.description : '', onCommit: (v) => patchDef({ description: v || undefined }), disabled: readOnly, rows: 2 }), defaultValueKind(type) && (_jsx(DefaultValueField, { kind: defaultValueKind(type), value: def.defaultValue, options: options, onCommit: (v) => patchDef({ defaultValue: v }), disabled: readOnly, locale: locale })), _jsx(TextareaField, { label: tr('designer.field.helpText'), value: typeof def.inlineHelpText === 'string' ? def.inlineHelpText : '', onCommit: (v) => patchDef({ inlineHelpText: v || undefined }), disabled: readOnly, rows: 2, placeholder: tr('designer.field.helpTextPlaceholder') })] }), (isPicklist(type) || isLookup(type) || isComputed(type) || isNumeric(type) || isTexty(type)) && (_jsxs(Section, { title: tFormat('designer.field.section.options', locale, { type: typeMetaLabel ?? type }), children: [isPicklist(type) && (_jsx(OptionsEditor, { options: options, onChange: patchOptions, disabled: readOnly, locale: locale })), isLookup(type) && (_jsxs(_Fragment, { children: [_jsx(ObjectPicker, { label: tr('designer.field.relatedObject'), value: typeof def.reference === 'string' ? def.reference : '', options: objectOptions, onCommit: (v) => patchDef({ reference: v || undefined }), disabled: readOnly, placeholder: tr('designer.field.objectNamePlaceholder') }), _jsx(InspectorTextField, { label: tr('designer.field.relationshipName'), value: typeof def.relationshipName === 'string' ? def.relationshipName : '', onCommit: (v) => patchDef({ relationshipName: v || undefined }), disabled: readOnly, placeholder: tr('designer.field.relationshipNameHint') }), _jsx(LookupConfigFields, { def: def, patchDef: patchDef, hostFieldNames: view.entries.map((e) => e.name).filter((n) => n !== entry.name), readOnly: readOnly })] })), isComputed(type) && (_jsx(TextareaField, { label: tr('designer.field.formula'), value: typeof def.formula === 'string' ? def.formula : '', onCommit: (v) => patchDef({ formula: v || undefined }), disabled: readOnly, rows: 4, mono: true, placeholder: "record.amount * 0.2" })), isNumeric(type) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(InspectorNumberField, { label: tr('designer.field.precision'), value: typeof def.precision === 'number' ? def.precision : undefined, onCommit: (v) => patchDef({ precision: v }), disabled: readOnly }), _jsx(InspectorNumberField, { label: tr('designer.field.scale'), value: typeof def.scale === 'number' ? def.scale : undefined, onCommit: (v) => patchDef({ scale: v }), disabled: readOnly }), _jsx(InspectorNumberField, { label: tr('designer.field.min'), value: typeof def.min === 'number' ? def.min : undefined, onCommit: (v) => patchDef({ min: v }), disabled: readOnly }), _jsx(InspectorNumberField, { label: tr('designer.field.max'), value: typeof def.max === 'number' ? def.max : undefined, onCommit: (v) => patchDef({ max: v }), disabled: readOnly })] })), isTexty(type) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(InspectorNumberField, { label: tr('designer.field.minLength'), value: typeof def.minLength === 'number' ? def.minLength : undefined, onCommit: (v) => patchDef({ minLength: v }), disabled: readOnly, placeholder: "0" }), _jsx(InspectorNumberField, { label: tr('designer.field.maxLength'), value: typeof def.maxLength === 'number' ? def.maxLength : undefined, onCommit: (v) => patchDef({ maxLength: v }), disabled: readOnly, placeholder: "255" })] }))] })), _jsxs(Section, { title: tr('designer.field.section.advanced'), children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(InspectorCheckboxField, { label: tr('designer.field.readonly'), value: !!def.readonly, onCommit: (v) => patchDef({ readonly: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.hidden'), value: !!def.hidden, onCommit: (v) => patchDef({ hidden: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.indexed'), value: !!def.indexed, onCommit: (v) => patchDef({ indexed: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.externalId'), value: !!def.externalId, onCommit: (v) => patchDef({ externalId: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.trackHistory'), value: !!def.trackHistory, onCommit: (v) => patchDef({ trackHistory: v || undefined }), disabled: readOnly })] }), _jsx(InspectorTextField, { label: tr('designer.field.placeholder'), value: typeof def.placeholder === 'string' ? def.placeholder : '', onCommit: (v) => patchDef({ placeholder: v || undefined }), disabled: readOnly }), _jsxs("div", { className: "space-y-1", children: [_jsx(InspectorTextField, { label: tr('designer.field.conditionalRequired'), value: typeof def.conditionalRequired === 'string' ? def.conditionalRequired : '', onCommit: (v) => patchDef({ conditionalRequired: v || undefined }), disabled: readOnly, mono: true, placeholder: "record.status == 'closed'" }), _jsx("p", { className: "text-[11px] text-muted-foreground/80 px-0.5 leading-snug", children: tr('designer.field.conditionalRequiredHint') })] }), fieldGroups.length > 0 && (_jsx(InspectorSelectField, { label: tr('designer.field.group'), value: typeof def.group === 'string' ? def.group : '', options: [
|
|
204
|
+
return (_jsxs(InspectorShell, { kindLabel: tr('designer.field.kind'), title: typeof def.label === 'string' && def.label ? def.label : entry.name, onClose: onClearSelection, closeLabel: tr('designer.field.close'), headerActions: headerActions, footer: footer, children: [_jsxs(Section, { title: tr('designer.field.section.basic'), children: [_jsx(InspectorTextField, { label: tr('designer.field.apiName'), value: entry.name, onCommit: setKey, disabled: readOnly, mono: true, testId: "field-apiname-input" }), _jsx(InspectorTextField, { label: tr('designer.field.label'), value: typeof def.label === 'string' ? def.label : '', onCommit: (v) => patchDef({ label: v }), onBlur: maybeDeriveName, disabled: readOnly, testId: "field-label-input" }), _jsx(InspectorSelectField, { label: tr('designer.field.type'), value: type, options: typeOptions, onCommit: (v) => patchDef({ type: v }), disabled: readOnly }), _jsxs("div", { className: "flex items-center gap-4 pt-1", children: [_jsx(InspectorCheckboxField, { label: tr('designer.field.required'), value: !!def.required, onCommit: (v) => patchDef({ required: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.unique'), value: !!def.unique, onCommit: (v) => patchDef({ unique: v || undefined }), disabled: readOnly })] }), _jsx(TextareaField, { label: tr('designer.field.description'), value: typeof def.description === 'string' ? def.description : '', onCommit: (v) => patchDef({ description: v || undefined }), disabled: readOnly, rows: 2 }), defaultValueKind(type) && (_jsx(DefaultValueField, { kind: defaultValueKind(type), value: def.defaultValue, options: options, onCommit: (v) => patchDef({ defaultValue: v }), disabled: readOnly, locale: locale })), _jsx(TextareaField, { label: tr('designer.field.helpText'), value: typeof def.inlineHelpText === 'string' ? def.inlineHelpText : '', onCommit: (v) => patchDef({ inlineHelpText: v || undefined }), disabled: readOnly, rows: 2, placeholder: tr('designer.field.helpTextPlaceholder') })] }), (isPicklist(type) || isLookup(type) || isComputed(type) || isNumeric(type) || isTexty(type)) && (_jsxs(Section, { title: tFormat('designer.field.section.options', locale, { type: typeMetaLabel ?? type }), children: [isPicklist(type) && (_jsx(OptionsEditor, { options: options, onChange: patchOptions, disabled: readOnly, locale: locale }, entry.name)), isLookup(type) && (_jsxs(_Fragment, { children: [_jsx(ObjectPicker, { label: tr('designer.field.relatedObject'), value: typeof def.reference === 'string' ? def.reference : '', options: objectOptions, onCommit: (v) => patchDef({ reference: v || undefined }), disabled: readOnly, placeholder: tr('designer.field.objectNamePlaceholder') }), _jsx(InspectorTextField, { label: tr('designer.field.relationshipName'), value: typeof def.relationshipName === 'string' ? def.relationshipName : '', onCommit: (v) => patchDef({ relationshipName: v || undefined }), disabled: readOnly, placeholder: tr('designer.field.relationshipNameHint') }), _jsx(LookupConfigFields, { def: def, patchDef: patchDef, hostFieldNames: view.entries.map((e) => e.name).filter((n) => n !== entry.name), readOnly: readOnly, locale: locale })] })), isComputed(type) && (_jsx(TextareaField, { label: tr('designer.field.formula'), value: typeof def.formula === 'string' ? def.formula : '', onCommit: (v) => patchDef({ formula: v || undefined }), disabled: readOnly, rows: 4, mono: true, placeholder: "record.amount * 0.2" })), isNumeric(type) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(InspectorNumberField, { label: tr('designer.field.precision'), value: typeof def.precision === 'number' ? def.precision : undefined, onCommit: (v) => patchDef({ precision: v }), disabled: readOnly }), _jsx(InspectorNumberField, { label: tr('designer.field.scale'), value: typeof def.scale === 'number' ? def.scale : undefined, onCommit: (v) => patchDef({ scale: v }), disabled: readOnly }), _jsx(InspectorNumberField, { label: tr('designer.field.min'), value: typeof def.min === 'number' ? def.min : undefined, onCommit: (v) => patchDef({ min: v }), disabled: readOnly }), _jsx(InspectorNumberField, { label: tr('designer.field.max'), value: typeof def.max === 'number' ? def.max : undefined, onCommit: (v) => patchDef({ max: v }), disabled: readOnly })] })), isTexty(type) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(InspectorNumberField, { label: tr('designer.field.minLength'), value: typeof def.minLength === 'number' ? def.minLength : undefined, onCommit: (v) => patchDef({ minLength: v }), disabled: readOnly, placeholder: "0" }), _jsx(InspectorNumberField, { label: tr('designer.field.maxLength'), value: typeof def.maxLength === 'number' ? def.maxLength : undefined, onCommit: (v) => patchDef({ maxLength: v }), disabled: readOnly, placeholder: "255" })] }))] })), _jsxs(Section, { title: tr('designer.field.section.advanced'), children: [_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsx(InspectorCheckboxField, { label: tr('designer.field.readonly'), value: !!def.readonly, onCommit: (v) => patchDef({ readonly: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.hidden'), value: !!def.hidden, onCommit: (v) => patchDef({ hidden: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.indexed'), value: !!def.indexed, onCommit: (v) => patchDef({ indexed: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.externalId'), value: !!def.externalId, onCommit: (v) => patchDef({ externalId: v || undefined }), disabled: readOnly }), _jsx(InspectorCheckboxField, { label: tr('designer.field.trackHistory'), value: !!def.trackHistory, onCommit: (v) => patchDef({ trackHistory: v || undefined }), disabled: readOnly })] }), _jsx(InspectorTextField, { label: tr('designer.field.placeholder'), value: typeof def.placeholder === 'string' ? def.placeholder : '', onCommit: (v) => patchDef({ placeholder: v || undefined }), disabled: readOnly }), _jsxs("div", { className: "space-y-1", children: [_jsx(InspectorTextField, { label: tr('designer.field.conditionalRequired'), value: typeof def.conditionalRequired === 'string' ? def.conditionalRequired : '', onCommit: (v) => patchDef({ conditionalRequired: v || undefined }), disabled: readOnly, mono: true, placeholder: "record.status == 'closed'" }), _jsx("p", { className: "text-[11px] text-muted-foreground/80 px-0.5 leading-snug", children: tr('designer.field.conditionalRequiredHint') })] }), fieldGroups.length > 0 && (_jsx(InspectorSelectField, { label: tr('designer.field.group'), value: typeof def.group === 'string' ? def.group : '', options: [
|
|
181
205
|
{ value: '', label: tr('designer.field.noGroup') },
|
|
182
206
|
...fieldGroups
|
|
183
207
|
.filter((g) => typeof g.key === 'string')
|
|
@@ -223,15 +247,25 @@ function ObjectPicker({ label, value, options, onCommit, disabled, placeholder,
|
|
|
223
247
|
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsx(Input, { list: listId, value: value, onChange: (e) => onCommit(e.target.value), disabled: disabled, className: "h-8 text-sm font-mono", placeholder: placeholder ?? 'object_name' }), _jsx("datalist", { id: listId, children: options.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })] }));
|
|
224
248
|
}
|
|
225
249
|
function OptionsEditor({ options, onChange, disabled, locale, }) {
|
|
250
|
+
// Local editing buffer. We keep a blank trailing row visible for input but
|
|
251
|
+
// only PERSIST rows whose `value` is non-empty — otherwise the blank row
|
|
252
|
+
// fails the spec identifier rule ("System identifier must be at least 2
|
|
253
|
+
// characters") and shows a confusing error mid-edit. The editor is remounted
|
|
254
|
+
// per field (key={entry.name}), so seeding from `options` once is correct.
|
|
255
|
+
const [rows, setRows] = React.useState(() => (options.length > 0 ? options : [{ value: '', label: '' }]));
|
|
256
|
+
const commit = (next) => {
|
|
257
|
+
setRows(next);
|
|
258
|
+
onChange(next.filter((o) => o.value.trim() !== ''));
|
|
259
|
+
};
|
|
226
260
|
const update = (i, patch) => {
|
|
227
|
-
const next = [...
|
|
261
|
+
const next = [...rows];
|
|
228
262
|
next[i] = { ...next[i], ...patch };
|
|
229
|
-
|
|
263
|
+
commit(next);
|
|
230
264
|
};
|
|
231
|
-
const remove = (i) =>
|
|
232
|
-
const move = (i, to) =>
|
|
233
|
-
const add = () =>
|
|
234
|
-
return (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: t('designer.field.picklistValues', locale) }), _jsx(Badge, { variant: "outline", className: "text-[10px]", children:
|
|
265
|
+
const remove = (i) => commit(rows.filter((_, j) => j !== i));
|
|
266
|
+
const move = (i, to) => commit(moveArray(rows, i, to));
|
|
267
|
+
const add = () => commit([...rows, { value: '', label: '' }]);
|
|
268
|
+
return (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: t('designer.field.picklistValues', locale) }), _jsx(Badge, { variant: "outline", className: "text-[10px]", children: rows.length })] }), rows.length === 0 ? (_jsx("div", { className: "text-[11px] italic text-muted-foreground px-1", children: t('designer.field.noValues', locale) })) : (_jsx("div", { className: "space-y-1", children: rows.map((o, i) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Input, { value: o.value, onChange: (e) => update(i, { value: e.target.value }), placeholder: t('designer.field.optValue', locale), disabled: disabled, className: "h-7 text-xs font-mono flex-1" }), _jsx(Input, { value: o.label ?? '', onChange: (e) => update(i, { label: e.target.value }), placeholder: t('designer.field.optLabel', locale), disabled: disabled, className: "h-7 text-xs flex-1" }), _jsx("input", { type: "color", value: o.color ?? '#cccccc', onChange: (e) => update(i, { color: e.target.value }), disabled: disabled, className: "h-7 w-7 rounded border bg-background cursor-pointer p-0.5", title: t('designer.field.optColor', locale) }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => move(i, i - 1), disabled: disabled || i === 0, "aria-label": t('designer.field.moveUp', locale), children: _jsx(ArrowUp, { className: "h-3 w-3" }) }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => move(i, i + 1), disabled: disabled || i === rows.length - 1, "aria-label": t('designer.field.moveDown', locale), children: _jsx(ArrowDown, { className: "h-3 w-3" }) }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-7 w-7 p-0 text-destructive", onClick: () => remove(i), disabled: disabled, "aria-label": t('designer.field.removeValue', locale), children: _jsx(X, { className: "h-3 w-3" }) })] }, i))) })), !disabled && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-7 gap-1 text-xs", onClick: add, children: [_jsx(Plus, { className: "h-3 w-3" }), t('designer.field.addValue', locale)] }))] }));
|
|
235
269
|
}
|
|
236
270
|
/* ─────────────── Lookup picker config (displayField / filters / dependent) ─────────────── */
|
|
237
271
|
const LOOKUP_OPERATORS = [
|
|
@@ -266,7 +300,8 @@ function readDependsOn(def) {
|
|
|
266
300
|
* dependent-lookup links to other fields on the same record (`dependsOn`).
|
|
267
301
|
* Every field choice is picked from the referenced object's live schema.
|
|
268
302
|
*/
|
|
269
|
-
function LookupConfigFields({ def, patchDef, hostFieldNames, readOnly, }) {
|
|
303
|
+
function LookupConfigFields({ def, patchDef, hostFieldNames, readOnly, locale, }) {
|
|
304
|
+
const tr = (key) => t(key, locale);
|
|
270
305
|
const reference = typeof def.reference === 'string' ? def.reference : undefined;
|
|
271
306
|
const { fields: targetFields, loading } = useObjectFields(reference);
|
|
272
307
|
const fieldOptions = React.useMemo(() => targetFields.filter((f) => !f.hidden).map((f) => ({ value: f.name, label: f.label, hint: f.type })), [targetFields]);
|
|
@@ -296,8 +331,8 @@ function LookupConfigFields({ def, patchDef, hostFieldNames, readOnly, }) {
|
|
|
296
331
|
const descriptionField = typeof def.descriptionField === 'string' ? def.descriptionField : '';
|
|
297
332
|
const pageSize = typeof def.lookupPageSize === 'number' ? def.lookupPageSize : undefined;
|
|
298
333
|
const allowCreate = def.allowCreate === true;
|
|
299
|
-
const fieldPlaceholder = reference ? '
|
|
300
|
-
return (_jsxs("div", { className: "space-y-2 border-t pt-2.5", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children:
|
|
334
|
+
const fieldPlaceholder = reference ? tr('designer.field.lookup.selectField') : tr('designer.field.lookup.setTargetFirst');
|
|
335
|
+
return (_jsxs("div", { className: "space-y-2 border-t pt-2.5", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: tr('designer.field.lookup.pickerConfig') }), _jsx(InspectorComboField, { label: tr('designer.field.lookup.displayField'), value: displayField, onCommit: (v) => patchDef({ displayField: v || undefined }), options: fieldOptions, loading: loading, placeholder: fieldPlaceholder, searchPlaceholder: tr('designer.field.lookup.searchFields'), disabled: readOnly, mono: true }), _jsx(InspectorComboField, { label: tr('designer.field.lookup.descriptionField'), value: descriptionField, onCommit: (v) => patchDef({ descriptionField: v || undefined }), options: fieldOptions, loading: loading, placeholder: fieldPlaceholder, searchPlaceholder: tr('designer.field.lookup.searchFields'), disabled: readOnly, mono: true }), _jsxs("div", { className: "space-y-1.5 pt-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: tr('designer.field.lookup.selectableRecords') }), _jsx(Badge, { variant: "outline", className: "text-[10px]", children: filters.length })] }), !readOnly && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 gap-1 px-1.5 text-[11px]", onClick: addFilter, children: [_jsx(Plus, { className: "h-3 w-3" }), " ", tr('designer.field.lookup.addFilter')] }))] }), filters.length === 0 ? (_jsx("p", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-center text-[11px] text-muted-foreground", children: tFormat('designer.field.lookup.noFilter', locale, { ref: reference || 'related' }) })) : (filters.map((f, i) => (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] text-muted-foreground", children: tFormat('designer.field.lookup.filterN', locale, { n: i + 1 }) }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": tr('designer.field.lookup.removeFilter'), className: "h-6 w-6 p-0 text-muted-foreground hover:text-destructive", onClick: () => removeFilter(i), children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorComboField, { label: tr('designer.field.lookup.filterField'), value: f.field ?? '', onCommit: (v) => patchFilter(i, { field: v }), options: fieldOptions, loading: loading, placeholder: fieldPlaceholder, searchPlaceholder: tr('designer.field.lookup.searchFields'), disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: tr('designer.field.lookup.filterOperator'), value: f.operator ?? 'eq', options: LOOKUP_OPERATORS, onCommit: (v) => patchFilter(i, { operator: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: tr('designer.field.lookup.filterValue'), value: valueToText(f.value), onCommit: (v) => patchFilter(i, { value: textToValue(f.operator, v) }), placeholder: f.operator === 'in' || f.operator === 'notIn' ? 'comma,separated,values' : 'value', disabled: readOnly, mono: true })] }, i))))] }), _jsxs("div", { className: "space-y-1.5 pt-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: tr('designer.field.lookup.dependsOn') }), dependsOn.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1", children: dependsOn.map((n) => (_jsxs("span", { className: "inline-flex items-center gap-1 rounded bg-secondary px-2 py-0.5 text-[11px] font-mono", children: [n, !readOnly && (_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground", onClick: () => removeDependsOn(n), "aria-label": `Remove ${n}`, children: "\u00D7" }))] }, n))) })), !readOnly && hostOptions.length > 0 && (_jsx(InspectorComboField, { value: "", onCommit: (v) => addDependsOn(v), options: hostOptions.filter((o) => !dependsOn.includes(o.value)), placeholder: tr('designer.field.lookup.addDependsOn'), searchPlaceholder: tr('designer.field.lookup.searchHostFields'), disabled: readOnly, allowCustom: false, mono: true }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 pt-1", children: [_jsx(InspectorNumberField, { label: tr('designer.field.lookup.pageSize'), value: pageSize, onCommit: (v) => patchDef({ lookupPageSize: v }), placeholder: "10", disabled: readOnly }), _jsx("div", { className: "flex items-end pb-1.5", children: _jsx(InspectorCheckboxField, { label: tr('designer.field.lookup.allowCreate'), value: allowCreate, onCommit: (v) => patchDef({ allowCreate: v || undefined }), disabled: readOnly }) })] })] }));
|
|
301
336
|
}
|
|
302
337
|
/* ─────────────── Hook: load object list for lookup picker ─────────────── */
|
|
303
338
|
function useObjectOptions() {
|
|
@@ -55,4 +55,4 @@ export declare function DatasetNamesEditor({ label, emptyText, names, options, l
|
|
|
55
55
|
readOnly?: boolean;
|
|
56
56
|
onCommit: (next: string[]) => void;
|
|
57
57
|
}): React.JSX.Element;
|
|
58
|
-
export declare function ReportDefaultInspector({ draft, onPatch, readOnly, locale, datasetCatalogOverride, serverSchema, }: ReportDefaultInspectorProps): React.JSX.Element;
|
|
58
|
+
export declare function ReportDefaultInspector({ name, draft, onPatch, readOnly, locale, datasetCatalogOverride, serverSchema, }: ReportDefaultInspectorProps): React.JSX.Element;
|
|
@@ -31,6 +31,7 @@ import * as React from 'react';
|
|
|
31
31
|
import { Badge, Label } from '@object-ui/components';
|
|
32
32
|
import { InspectorShell, InspectorTextField, InspectorSelectField, appendArray, moveArray, spliceArray, } from './_shared';
|
|
33
33
|
import { AddFieldPopover, FieldListRow } from '../previews/ViewColumnPanes';
|
|
34
|
+
import { toFieldName } from '../previews/object-fields-io';
|
|
34
35
|
import { SchemaForm } from '../SchemaForm';
|
|
35
36
|
import { useDatasetCatalog, useDatasetSemantics, } from '../previews/useDatasetCatalog';
|
|
36
37
|
import { getReportForm, getReportSchema } from '../report-schema';
|
|
@@ -50,7 +51,14 @@ const REPORT_CURATED_FIELDS = new Set([
|
|
|
50
51
|
'values',
|
|
51
52
|
'rows',
|
|
52
53
|
'columns', // matrix across-dimensions — dedicated list below
|
|
54
|
+
'chart', // dedicated Chart panel below (type + dataset-aware X/Y pickers)
|
|
53
55
|
]);
|
|
56
|
+
/**
|
|
57
|
+
* Chart types offered in the curated Chart panel. A dataset-bound report plots
|
|
58
|
+
* one measure (yAxis) across one dimension (xAxis), so we surface the families
|
|
59
|
+
* that fit that shape; the renderer maps the rest. (`''` = no chart / table-only.)
|
|
60
|
+
*/
|
|
61
|
+
const REPORT_CHART_TYPES = ['bar', 'column', 'line', 'area', 'pie', 'donut'];
|
|
54
62
|
/** i18n keys for the spec `type` enum (falls back to the raw value). */
|
|
55
63
|
const TYPE_LABEL_KEYS = {
|
|
56
64
|
tabular: 'engine.inspector.report.type.tabular',
|
|
@@ -108,8 +116,17 @@ export function DatasetNamesEditor({ label, emptyText, names, options, loading,
|
|
|
108
116
|
setOverIndex(null);
|
|
109
117
|
} }, `${name}-${i}`))) })), !readOnly && (_jsx(AddFieldPopover, { fields: options, usedNames: used, loading: loading, error: error, onAdd: (f) => onCommit(appendArray(names, f.name)) }))] }));
|
|
110
118
|
}
|
|
111
|
-
export function ReportDefaultInspector({ draft, onPatch, readOnly, locale, datasetCatalogOverride, serverSchema, }) {
|
|
119
|
+
export function ReportDefaultInspector({ name, draft, onPatch, readOnly, locale, datasetCatalogOverride, serverSchema, }) {
|
|
112
120
|
const tr = React.useCallback((key) => t(key, locale), [locale]);
|
|
121
|
+
// In create mode the host passes an empty `name` (the PK is assigned on
|
|
122
|
+
// first save). Mirror ObjectDefaultInspector: expose an editable Name that
|
|
123
|
+
// auto-derives a snake_case slug from the label until the author edits it
|
|
124
|
+
// directly. Without this, a report created through the canvas would save
|
|
125
|
+
// with an empty name and fail the snake_case identity rule (the create flow
|
|
126
|
+
// would dead-end exactly the way it did before report-create used the canvas).
|
|
127
|
+
const createMode = !name;
|
|
128
|
+
const nameTouched = React.useRef(false);
|
|
129
|
+
const nameValue = typeof draft.name === 'string' ? draft.name : '';
|
|
113
130
|
const reportType = typeof draft.type === 'string' ? draft.type : 'tabular';
|
|
114
131
|
const typeOptions = useTypeOptions(reportType, locale);
|
|
115
132
|
const labelValue = typeof draft.label === 'string' ? draft.label : '';
|
|
@@ -143,6 +160,34 @@ export function ReportDefaultInspector({ draft, onPatch, readOnly, locale, datas
|
|
|
143
160
|
type: d.type ?? 'text',
|
|
144
161
|
hidden: false,
|
|
145
162
|
})), [semantics.dimensions]);
|
|
163
|
+
// Embedded chart (ADR-0021) — edited via the dedicated panel below so authors
|
|
164
|
+
// pick the X dimension / Y measure from dropdowns sourced from the bound
|
|
165
|
+
// dataset (instead of free-typing field names), and the generic spec-form
|
|
166
|
+
// graft excludes `chart`. Patching merges into the chart object; clearing the
|
|
167
|
+
// type drops the chart entirely.
|
|
168
|
+
const chart = draft.chart && typeof draft.chart === 'object'
|
|
169
|
+
? draft.chart
|
|
170
|
+
: {};
|
|
171
|
+
const chartType = typeof chart.type === 'string' ? chart.type : '';
|
|
172
|
+
const chartX = typeof chart.xAxis === 'string' ? chart.xAxis : '';
|
|
173
|
+
const chartY = typeof chart.yAxis === 'string' ? chart.yAxis : '';
|
|
174
|
+
const chartTitle = typeof chart.title === 'string' ? chart.title : '';
|
|
175
|
+
const commitChart = (patch) => {
|
|
176
|
+
const next = { ...chart, ...patch };
|
|
177
|
+
onPatch({ chart: next.type ? next : undefined });
|
|
178
|
+
};
|
|
179
|
+
const chartXOptions = React.useMemo(() => {
|
|
180
|
+
const opts = dimensionOptions.map((d) => ({ value: d.name, label: d.label || d.name }));
|
|
181
|
+
if (chartX && !opts.some((o) => o.value === chartX))
|
|
182
|
+
opts.push({ value: chartX, label: chartX });
|
|
183
|
+
return opts;
|
|
184
|
+
}, [dimensionOptions, chartX]);
|
|
185
|
+
const chartYOptions = React.useMemo(() => {
|
|
186
|
+
const opts = measureOptions.map((m) => ({ value: m.name, label: m.label || m.name }));
|
|
187
|
+
if (chartY && !opts.some((o) => o.value === chartY))
|
|
188
|
+
opts.push({ value: chartY, label: chartY });
|
|
189
|
+
return opts;
|
|
190
|
+
}, [measureOptions, chartY]);
|
|
146
191
|
// A `joined` report carries its data on dataset-bound `blocks` (edited via
|
|
147
192
|
// the spec form's repeater) — the top-level binding only applies otherwise.
|
|
148
193
|
const datasetBound = reportType !== 'joined';
|
|
@@ -156,5 +201,18 @@ export function ReportDefaultInspector({ draft, onPatch, readOnly, locale, datas
|
|
|
156
201
|
excludeFields: REPORT_CURATED_FIELDS,
|
|
157
202
|
sectionTitle: t('engine.inspector.moreFields', locale),
|
|
158
203
|
}), [serverSchema, locale]);
|
|
159
|
-
return (_jsxs(InspectorShell, { kindLabel: tr('engine.inspector.report.kind'), title: String(labelValue || draft.name || tr('engine.inspector.report.kind')), onClose: () => { }, closeLabel: tr('engine.inspector.report.close'), hideClose: true, children: [
|
|
204
|
+
return (_jsxs(InspectorShell, { kindLabel: tr('engine.inspector.report.kind'), title: String(labelValue || draft.name || tr('engine.inspector.report.kind')), onClose: () => { }, closeLabel: tr('engine.inspector.report.close'), hideClose: true, children: [createMode && (_jsx(InspectorTextField, { label: tr('engine.inspector.report.name'), value: nameValue, onCommit: (v) => {
|
|
205
|
+
nameTouched.current = true;
|
|
206
|
+
onPatch({ name: toFieldName(v) });
|
|
207
|
+
}, placeholder: tr('engine.inspector.report.namePlaceholder'), disabled: readOnly, mono: true })), _jsx(InspectorTextField, { label: tr('engine.inspector.report.label'), value: labelValue, onCommit: (v) => {
|
|
208
|
+
// Live-derive the snake_case name from the label until the author
|
|
209
|
+
// edits the Name field directly (create mode only).
|
|
210
|
+
const patch = { label: v };
|
|
211
|
+
if (createMode && !nameTouched.current)
|
|
212
|
+
patch.name = toFieldName(v);
|
|
213
|
+
onPatch(patch);
|
|
214
|
+
}, placeholder: tr('engine.inspector.report.labelPlaceholder'), disabled: readOnly }), _jsx(InspectorSelectField, { label: tr('engine.inspector.report.type'), value: reportType, options: typeOptions, onCommit: (v) => onPatch({ type: v }), disabled: readOnly }), datasetBound && (_jsxs(_Fragment, { children: [catalog.datasets.length > 0 || datasetName ? (_jsx(InspectorSelectField, { label: tr('engine.inspector.report.dataset'), value: datasetName, options: datasetOptions, onCommit: (v) => onPatch({ dataset: v }), disabled: readOnly })) : (_jsx(InspectorTextField, { label: tr('engine.inspector.report.dataset'), value: datasetName, onCommit: (v) => onPatch({ dataset: v }), placeholder: tr('engine.inspector.report.datasetPlaceholder'), disabled: readOnly, mono: true })), _jsx(DatasetNamesEditor, { label: tr('engine.inspector.report.values'), emptyText: tr('engine.inspector.report.valuesEmpty'), names: values, options: measureOptions, loading: semantics.loading, error: semantics.error, readOnly: readOnly, onCommit: (next) => onPatch({ values: next }) }), _jsx(DatasetNamesEditor, { label: tr('engine.inspector.report.rows'), emptyText: tr('engine.inspector.report.rowsEmpty'), names: rows, options: dimensionOptions, loading: semantics.loading, error: semantics.error, readOnly: readOnly, onCommit: (next) => onPatch({ rows: next }) }), reportType === 'matrix' && (_jsx(DatasetNamesEditor, { label: tr('engine.inspector.report.columnsAcross'), emptyText: tr('engine.inspector.report.columnsAcrossEmpty'), names: columnsAcross, options: dimensionOptions, loading: semantics.loading, error: semantics.error, readOnly: readOnly, onCommit: (next) => onPatch({ columns: next }) })), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: tr('engine.inspector.report.chart') }), _jsx(InspectorSelectField, { label: tr('engine.inspector.report.chartType'), value: chartType, options: [
|
|
215
|
+
{ value: '', label: tr('engine.inspector.report.chartNone') },
|
|
216
|
+
...REPORT_CHART_TYPES.map((tp) => ({ value: tp, label: tp })),
|
|
217
|
+
], onCommit: (v) => commitChart({ type: v || undefined }), disabled: readOnly }), chartType ? (_jsxs(_Fragment, { children: [_jsx(InspectorTextField, { label: tr('engine.inspector.report.chartTitle'), value: chartTitle, onCommit: (v) => commitChart({ title: v || undefined }), disabled: readOnly }), _jsx(InspectorSelectField, { label: tr('engine.inspector.report.chartX'), value: chartX, options: chartXOptions, onCommit: (v) => commitChart({ xAxis: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: tr('engine.inspector.report.chartY'), value: chartY, options: chartYOptions, onCommit: (v) => commitChart({ yAxis: v }), disabled: readOnly })] })) : null] })] })), _jsx("div", { className: "border-t pt-3", children: schema ? (_jsx(SchemaForm, { schema: schema, form: form, value: draft, hiddenFields: ['type', 'label', 'name', 'dataset', 'values', 'rows', 'columns', 'chart'], readOnly: readOnly, onChange: (next) => onPatch(next) })) : (_jsx("p", { className: "text-[11px] text-muted-foreground", children: tr('engine.inspector.report.noSchema') })) })] }));
|
|
160
218
|
}
|
|
@@ -54,13 +54,17 @@ export interface InspectorReorderButtonsProps {
|
|
|
54
54
|
* `total - 1`) and the whole pair when `total <= 1`.
|
|
55
55
|
*/
|
|
56
56
|
export declare function InspectorReorderButtons({ index, total, onMove, upLabel, downLabel, disabled, }: InspectorReorderButtonsProps): React.JSX.Element | null;
|
|
57
|
-
export declare function InspectorTextField({ label, value, onCommit, placeholder, disabled, mono, }: {
|
|
57
|
+
export declare function InspectorTextField({ label, value, onCommit, onBlur, placeholder, disabled, mono, testId, }: {
|
|
58
58
|
label: string;
|
|
59
59
|
value: string;
|
|
60
60
|
onCommit: (v: string) => void;
|
|
61
|
+
/** Fired on blur with the final value — e.g. to derive a dependent field. */
|
|
62
|
+
onBlur?: (v: string) => void;
|
|
61
63
|
placeholder?: string;
|
|
62
64
|
disabled?: boolean;
|
|
63
65
|
mono?: boolean;
|
|
66
|
+
/** Stable hook for e2e/dogfood selectors. */
|
|
67
|
+
testId?: string;
|
|
64
68
|
}): React.JSX.Element;
|
|
65
69
|
export declare function InspectorNumberField({ label, value, onCommit, placeholder, disabled, }: {
|
|
66
70
|
label: string;
|
|
@@ -18,8 +18,8 @@ export function InspectorReorderButtons({ index, total, onMove, upLabel = 'Move
|
|
|
18
18
|
return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => canUp && onMove(index - 1), disabled: !canUp, "aria-label": upLabel, title: upLabel, children: _jsx(ArrowUp, { className: "h-4 w-4" }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 p-0", onClick: () => canDown && onMove(index + 1), disabled: !canDown, "aria-label": downLabel, title: downLabel, children: _jsx(ArrowDown, { className: "h-4 w-4" }) })] }));
|
|
19
19
|
}
|
|
20
20
|
/* ─────────────── Form atoms ─────────────── */
|
|
21
|
-
export function InspectorTextField({ label, value, onCommit, placeholder, disabled, mono, }) {
|
|
22
|
-
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsx(Input, { value: value, onChange: (e) => onCommit(e.target.value), placeholder: placeholder, disabled: disabled, className: cn('h-8 text-sm', mono && 'font-mono') })] }));
|
|
21
|
+
export function InspectorTextField({ label, value, onCommit, onBlur, placeholder, disabled, mono, testId, }) {
|
|
22
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsx(Input, { value: value, onChange: (e) => onCommit(e.target.value), onBlur: (e) => onBlur?.(e.target.value), placeholder: placeholder, disabled: disabled, "data-testid": testId, className: cn('h-8 text-sm', mono && 'font-mono') })] }));
|
|
23
23
|
}
|
|
24
24
|
export function InspectorNumberField({ label, value, onCommit, placeholder, disabled, }) {
|
|
25
25
|
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsx(Input, { type: "number", value: value ?? '', onChange: (e) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface BuilderCondition {
|
|
2
|
+
id?: string;
|
|
3
|
+
field: string;
|
|
4
|
+
operator: string;
|
|
5
|
+
value?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface BuilderGroup {
|
|
8
|
+
id?: string;
|
|
9
|
+
logic: 'and' | 'or';
|
|
10
|
+
conditions: BuilderCondition[];
|
|
11
|
+
}
|
|
12
|
+
export type FilterCondition = Record<string, any>;
|
|
13
|
+
/** Serialize the visual group → a spec FilterCondition (flat `$and`). */
|
|
14
|
+
export declare function groupToCondition(group: BuilderGroup | undefined): FilterCondition | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Parse a stored FilterCondition → the visual group. `representable: false` when
|
|
17
|
+
* the condition uses shapes the flat builder can't faithfully edit (nested
|
|
18
|
+
* `$and`/`$or`, multi-op objects, unmapped operators) — callers should then show
|
|
19
|
+
* the source editor instead.
|
|
20
|
+
*/
|
|
21
|
+
export declare function conditionToGroup(cond: FilterCondition | undefined | null): {
|
|
22
|
+
group: BuilderGroup;
|
|
23
|
+
representable: boolean;
|
|
24
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Bridge between the visual {@link FilterBuilder} (a flat `FilterGroup` of
|
|
4
|
+
* `{field, operator, value}` rows, camelCase operators) and the spec
|
|
5
|
+
* `FilterCondition` (Mongo-style `{ field: { $op: value } }`, conjoined with
|
|
6
|
+
* `$and`) stored on `dataset.filter` / `measure.filter`.
|
|
7
|
+
*
|
|
8
|
+
* Scope (deliberate): the visual editor supports the common case — a flat AND
|
|
9
|
+
* of simple `field op value` conditions. Anything it can't faithfully round-trip
|
|
10
|
+
* (nested groups, `$or`, multi-operator objects, unmapped operators) is reported
|
|
11
|
+
* as NOT representable so the caller can fall back to the source editor instead
|
|
12
|
+
* of silently corrupting the author's filter.
|
|
13
|
+
*/
|
|
14
|
+
/** FilterBuilder camelCase operator → FilterCondition Mongo operator. */
|
|
15
|
+
const OP_TO_MONGO = {
|
|
16
|
+
equals: '$eq', notEquals: '$ne',
|
|
17
|
+
greaterThan: '$gt', greaterOrEqual: '$gte', lessThan: '$lt', lessOrEqual: '$lte',
|
|
18
|
+
after: '$gt', before: '$lt',
|
|
19
|
+
contains: '$contains', in: '$in', notIn: '$nin',
|
|
20
|
+
};
|
|
21
|
+
const MONGO_TO_OP = {
|
|
22
|
+
$eq: 'equals', $ne: 'notEquals',
|
|
23
|
+
$gt: 'greaterThan', $gte: 'greaterOrEqual', $lt: 'lessThan', $lte: 'lessOrEqual',
|
|
24
|
+
$contains: 'contains', $in: 'in', $nin: 'notIn',
|
|
25
|
+
};
|
|
26
|
+
/** Serialize the visual group → a spec FilterCondition (flat `$and`). */
|
|
27
|
+
export function groupToCondition(group) {
|
|
28
|
+
const conds = (group?.conditions ?? []).filter((c) => c && c.field);
|
|
29
|
+
const parts = [];
|
|
30
|
+
for (const c of conds) {
|
|
31
|
+
if (c.operator === 'isEmpty') {
|
|
32
|
+
parts.push({ [c.field]: { $exists: false } });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (c.operator === 'isNotEmpty') {
|
|
36
|
+
parts.push({ [c.field]: { $exists: true } });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const mop = OP_TO_MONGO[c.operator];
|
|
40
|
+
if (!mop)
|
|
41
|
+
continue; // unmapped (e.g. notContains/between) — drop rather than emit a bad filter
|
|
42
|
+
parts.push({ [c.field]: { [mop]: c.value } });
|
|
43
|
+
}
|
|
44
|
+
if (parts.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (parts.length === 1)
|
|
47
|
+
return parts[0];
|
|
48
|
+
return { $and: parts };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse a stored FilterCondition → the visual group. `representable: false` when
|
|
52
|
+
* the condition uses shapes the flat builder can't faithfully edit (nested
|
|
53
|
+
* `$and`/`$or`, multi-op objects, unmapped operators) — callers should then show
|
|
54
|
+
* the source editor instead.
|
|
55
|
+
*/
|
|
56
|
+
export function conditionToGroup(cond) {
|
|
57
|
+
const empty = { id: 'g', logic: 'and', conditions: [] };
|
|
58
|
+
if (cond == null)
|
|
59
|
+
return { group: empty, representable: true };
|
|
60
|
+
if (typeof cond !== 'object' || Array.isArray(cond))
|
|
61
|
+
return { group: empty, representable: false };
|
|
62
|
+
if ('$or' in cond)
|
|
63
|
+
return { group: empty, representable: false };
|
|
64
|
+
const list = Array.isArray(cond.$and) ? cond.$and : [cond];
|
|
65
|
+
const conditions = [];
|
|
66
|
+
for (let i = 0; i < list.length; i++) {
|
|
67
|
+
const c = list[i];
|
|
68
|
+
if (!c || typeof c !== 'object' || Array.isArray(c))
|
|
69
|
+
return { group: empty, representable: false };
|
|
70
|
+
if ('$and' in c || '$or' in c)
|
|
71
|
+
return { group: empty, representable: false };
|
|
72
|
+
const keys = Object.keys(c);
|
|
73
|
+
if (keys.length !== 1)
|
|
74
|
+
return { group: empty, representable: false };
|
|
75
|
+
const field = keys[0];
|
|
76
|
+
const v = c[field];
|
|
77
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
78
|
+
const opKeys = Object.keys(v);
|
|
79
|
+
if (opKeys.length !== 1)
|
|
80
|
+
return { group: empty, representable: false };
|
|
81
|
+
const mop = opKeys[0];
|
|
82
|
+
if (mop === '$exists') {
|
|
83
|
+
conditions.push({ id: `c${i}`, field, operator: v.$exists ? 'isNotEmpty' : 'isEmpty', value: '' });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const op = MONGO_TO_OP[mop];
|
|
87
|
+
if (!op)
|
|
88
|
+
return { group: empty, representable: false };
|
|
89
|
+
conditions.push({ id: `c${i}`, field, operator: op, value: v[mop] });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
conditions.push({ id: `c${i}`, field, operator: 'equals', value: v }); // implicit equality
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { group: { id: 'g', logic: 'and', conditions }, representable: true };
|
|
97
|
+
}
|