@object-ui/app-shell 7.0.0 → 7.2.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 +560 -0
- package/dist/console/AppContent.js +23 -17
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +47 -16
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +42 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -2
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +41 -10
- package/dist/layout/ConsoleLayout.js +5 -6
- 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/DraftPreviewBar.js +20 -7
- 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/ExpressionProvider.js +9 -3
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -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.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +58 -5
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
- package/dist/views/metadata-admin/ResourceListPage.js +28 -19
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +108 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- 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/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- 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 +102 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- 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 +41 -0
- package/dist/views/metadata-admin/package-scope.js +59 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- 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 +17 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- 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 +20 -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 +76 -2
- 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
|
@@ -15,6 +15,8 @@ import { Plus, X } from 'lucide-react';
|
|
|
15
15
|
import { Button, Input, Label, Checkbox } from '@object-ui/components';
|
|
16
16
|
import { uniqueId } from './_shared';
|
|
17
17
|
import { ReferenceCombobox, resolveRefKind } from './FlowReferenceField';
|
|
18
|
+
import { VariableTextInput } from './VariableTextInput';
|
|
19
|
+
import { FlowExprIssue } from './FlowExprIssue';
|
|
18
20
|
function toRows(list, columns) {
|
|
19
21
|
const ids = [];
|
|
20
22
|
return list.map((item) => {
|
|
@@ -56,7 +58,7 @@ function rowsToList(rows, columns) {
|
|
|
56
58
|
}
|
|
57
59
|
return out;
|
|
58
60
|
}
|
|
59
|
-
export function FlowObjectListField({ label, columns, value, onCommit, disabled, addLabel, removeLabel, emptyLabel, context, }) {
|
|
61
|
+
export function FlowObjectListField({ label, columns, value, onCommit, disabled, addLabel, removeLabel, emptyLabel, context, scopeGroups, }) {
|
|
60
62
|
const external = React.useMemo(() => Array.isArray(value)
|
|
61
63
|
? value.filter((v) => v && typeof v === 'object')
|
|
62
64
|
: [], [value]);
|
|
@@ -98,8 +100,11 @@ export function FlowObjectListField({ label, columns, value, onCommit, disabled,
|
|
|
98
100
|
flush(next);
|
|
99
101
|
return next;
|
|
100
102
|
});
|
|
101
|
-
}, disabled: disabled })) : col.kind === 'reference' ? (_jsx("div", { className: "flex-1", children: _jsx(ReferenceCombobox, { resolved: resolveRefKind(col.ref, (k) => row.values[k]), value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onCommit: (v) => setCell(row.id, col.key, typeof v === 'string' ? v : ''), onBlur: () => flush(rows), placeholder: col.placeholder, disabled: disabled, context: context, showHint: false }) })) : (_jsx(
|
|
103
|
+
}, disabled: disabled })) : col.kind === 'reference' ? (_jsx("div", { className: "flex-1", children: _jsx(ReferenceCombobox, { resolved: resolveRefKind(col.ref, (k) => row.values[k]), value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onCommit: (v) => setCell(row.id, col.key, typeof v === 'string' ? v : ''), onBlur: () => flush(rows), placeholder: col.placeholder, disabled: disabled, context: context, showHint: false }) })) : col.kind === 'expression' ? (_jsxs("div", { className: "flex-1 space-y-1", children: [_jsx(VariableTextInput, { mode: "expression", mono: true, value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onValueChange: (v) => setCell(row.id, col.key, v), onBlur: () => flush(rows), onKeyDown: (e) => {
|
|
104
|
+
if (e.key === 'Enter')
|
|
105
|
+
e.target.blur();
|
|
106
|
+
}, groups: scopeGroups ?? [], placeholder: col.placeholder, disabled: disabled }), _jsx(FlowExprIssue, { value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', role: "predicate", scopeGroups: scopeGroups })] })) : (_jsx(Input, { value: typeof row.values[col.key] === 'string' ? row.values[col.key] : '', onChange: (e) => setCell(row.id, col.key, e.target.value), onBlur: () => flush(rows), onKeyDown: (e) => {
|
|
102
107
|
if (e.key === 'Enter')
|
|
103
108
|
e.target.blur();
|
|
104
|
-
}, placeholder: col.placeholder, disabled: disabled, className:
|
|
109
|
+
}, placeholder: col.placeholder, disabled: disabled, className: "h-8 flex-1 text-xs" }))] }, col.key))) })] }, row.id)))] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "h-7 w-full text-xs", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }), addLabel] })] }));
|
|
105
110
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VariableTextInput — a single-line input (or textarea) for an expression /
|
|
3
|
+
* template flow-config value, with a "{x}" data-picker (#1934) that inserts an
|
|
4
|
+
* in-scope reference at the cursor.
|
|
5
|
+
*
|
|
6
|
+
* Brace handling is done FOR the author (ADR-0032): the picker inserts the BARE
|
|
7
|
+
* reference (`discount_pct`) in `expression` fields and the braced
|
|
8
|
+
* `{discount_pct}` in `template` (text / textarea) fields — killing the
|
|
9
|
+
* recurring `{record.x}` brace-in-CEL trap. Free-text typing is untouched, and
|
|
10
|
+
* the picker button is hidden entirely when nothing is in scope, so an empty
|
|
11
|
+
* scope degrades to a plain input.
|
|
12
|
+
*/
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import type { ScopeGroup } from './useFlowScope';
|
|
15
|
+
export type VariableFieldMode = 'expression' | 'template';
|
|
16
|
+
/** Wrap a bare reference token for insertion into the given field mode. */
|
|
17
|
+
export declare function formatToken(token: string, mode: VariableFieldMode): string;
|
|
18
|
+
/**
|
|
19
|
+
* Splice a reference into `value` at the selection `[selStart, selEnd]`, in the
|
|
20
|
+
* brace mode for the field, returning the new string and the caret position
|
|
21
|
+
* just after the inserted token. Pure (the DOM caret restore lives in the
|
|
22
|
+
* component); selection bounds are clamped and order-normalized so a reversed or
|
|
23
|
+
* out-of-range selection can't corrupt the value.
|
|
24
|
+
*/
|
|
25
|
+
export declare function insertToken(value: string, mode: VariableFieldMode, token: string, selStart: number, selEnd: number): {
|
|
26
|
+
next: string;
|
|
27
|
+
caret: number;
|
|
28
|
+
};
|
|
29
|
+
export interface VariableTextInputProps {
|
|
30
|
+
value: string;
|
|
31
|
+
onValueChange: (v: string) => void;
|
|
32
|
+
onBlur?: () => void;
|
|
33
|
+
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
34
|
+
/** Brace rule: bare token for `expression`, `{token}` for `template`. */
|
|
35
|
+
mode: VariableFieldMode;
|
|
36
|
+
/** In-scope reference groups (from useFlowScope). Empty → no picker button. */
|
|
37
|
+
groups: ScopeGroup[];
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/** Render a multi-line `<textarea>` instead of a single-line input. */
|
|
41
|
+
multiline?: boolean;
|
|
42
|
+
rows?: number;
|
|
43
|
+
/** Monospace the input text (expressions). Textareas are always mono. */
|
|
44
|
+
mono?: boolean;
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function VariableTextInput({ value, onValueChange, onBlur, onKeyDown, mode, groups, placeholder, disabled, multiline, rows, mono, className, }: VariableTextInputProps): React.JSX.Element;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* VariableTextInput — a single-line input (or textarea) for an expression /
|
|
5
|
+
* template flow-config value, with a "{x}" data-picker (#1934) that inserts an
|
|
6
|
+
* in-scope reference at the cursor.
|
|
7
|
+
*
|
|
8
|
+
* Brace handling is done FOR the author (ADR-0032): the picker inserts the BARE
|
|
9
|
+
* reference (`discount_pct`) in `expression` fields and the braced
|
|
10
|
+
* `{discount_pct}` in `template` (text / textarea) fields — killing the
|
|
11
|
+
* recurring `{record.x}` brace-in-CEL trap. Free-text typing is untouched, and
|
|
12
|
+
* the picker button is hidden entirely when nothing is in scope, so an empty
|
|
13
|
+
* scope degrades to a plain input.
|
|
14
|
+
*/
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { Braces } from 'lucide-react';
|
|
17
|
+
import { cn, Input, Popover, PopoverTrigger, PopoverContent, Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, } from '@object-ui/components';
|
|
18
|
+
/** Wrap a bare reference token for insertion into the given field mode. */
|
|
19
|
+
export function formatToken(token, mode) {
|
|
20
|
+
return mode === 'template' ? `{${token}}` : token;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Splice a reference into `value` at the selection `[selStart, selEnd]`, in the
|
|
24
|
+
* brace mode for the field, returning the new string and the caret position
|
|
25
|
+
* just after the inserted token. Pure (the DOM caret restore lives in the
|
|
26
|
+
* component); selection bounds are clamped and order-normalized so a reversed or
|
|
27
|
+
* out-of-range selection can't corrupt the value.
|
|
28
|
+
*/
|
|
29
|
+
export function insertToken(value, mode, token, selStart, selEnd) {
|
|
30
|
+
const text = formatToken(token, mode);
|
|
31
|
+
const a = Math.min(Math.max(selStart, 0), value.length);
|
|
32
|
+
const b = Math.min(Math.max(selEnd, 0), value.length);
|
|
33
|
+
const lo = Math.min(a, b);
|
|
34
|
+
const hi = Math.max(a, b);
|
|
35
|
+
return { next: value.slice(0, lo) + text + value.slice(hi), caret: lo + text.length };
|
|
36
|
+
}
|
|
37
|
+
const PICK_LABEL = 'Insert a reference';
|
|
38
|
+
const SEARCH_LABEL = 'Search references…';
|
|
39
|
+
const EMPTY_LABEL = 'No matching references.';
|
|
40
|
+
export function VariableTextInput({ value, onValueChange, onBlur, onKeyDown, mode, groups, placeholder, disabled, multiline, rows = 4, mono, className, }) {
|
|
41
|
+
const inputRef = React.useRef(null);
|
|
42
|
+
// Remember the caret across the button press (which blurs the field) so the
|
|
43
|
+
// token lands where the author was typing — not appended at the end.
|
|
44
|
+
const caret = React.useRef({ start: 0, end: 0 });
|
|
45
|
+
const [open, setOpen] = React.useState(false);
|
|
46
|
+
const setRef = (el) => {
|
|
47
|
+
inputRef.current = el;
|
|
48
|
+
};
|
|
49
|
+
const rememberCaret = () => {
|
|
50
|
+
const el = inputRef.current;
|
|
51
|
+
if (el) {
|
|
52
|
+
caret.current = {
|
|
53
|
+
start: el.selectionStart ?? value.length,
|
|
54
|
+
end: el.selectionEnd ?? value.length,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const insert = (token) => {
|
|
59
|
+
const { next, caret: pos } = insertToken(value, mode, token, caret.current.start, caret.current.end);
|
|
60
|
+
onValueChange(next);
|
|
61
|
+
setOpen(false);
|
|
62
|
+
// Restore focus + place the caret just after the inserted token.
|
|
63
|
+
requestAnimationFrame(() => {
|
|
64
|
+
const el = inputRef.current;
|
|
65
|
+
if (!el)
|
|
66
|
+
return;
|
|
67
|
+
el.focus();
|
|
68
|
+
try {
|
|
69
|
+
el.setSelectionRange(pos, pos);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* some input types disallow setSelectionRange */
|
|
73
|
+
}
|
|
74
|
+
caret.current = { start: pos, end: pos };
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const hasScope = groups.some((g) => g.refs.length > 0);
|
|
78
|
+
const shared = {
|
|
79
|
+
value,
|
|
80
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
81
|
+
onBlur,
|
|
82
|
+
onKeyDown,
|
|
83
|
+
onSelect: rememberCaret,
|
|
84
|
+
onKeyUp: rememberCaret,
|
|
85
|
+
onClick: rememberCaret,
|
|
86
|
+
placeholder,
|
|
87
|
+
disabled,
|
|
88
|
+
};
|
|
89
|
+
return (_jsxs("div", { className: cn('relative', className), children: [multiline ? (_jsx("textarea", { ref: setRef, ...shared, rows: rows, className: "w-full rounded border bg-background px-2 py-1.5 pr-8 font-mono text-xs" })) : (_jsx(Input, { ref: setRef, ...shared, className: cn('h-8 pr-8 text-sm', mono && 'font-mono') })), hasScope && !disabled && (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": PICK_LABEL, title: PICK_LABEL,
|
|
90
|
+
// Capture the caret on mousedown (fires before the input blur), so
|
|
91
|
+
// the insertion point is the author's last position.
|
|
92
|
+
onMouseDown: rememberCaret, className: "absolute right-1 top-1 inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", children: _jsx(Braces, { className: "h-3.5 w-3.5" }) }) }), _jsx(PopoverContent, { align: "end", className: "w-72 p-0",
|
|
93
|
+
// Keep our rAF focus-restore from fighting Radix's focus return.
|
|
94
|
+
onCloseAutoFocus: (e) => e.preventDefault(), children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: SEARCH_LABEL, className: "h-9" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: EMPTY_LABEL }), groups.map((g) => (_jsx(CommandGroup, { heading: g.label, children: g.refs.map((ref) => (_jsxs(CommandItem, { value: `${ref.token} ${ref.label} ${ref.detail ?? ''}`, onSelect: () => insert(ref.token), className: "flex items-center justify-between gap-2", children: [_jsx("span", { className: "truncate font-mono text-xs", children: formatToken(ref.token, mode) }), ref.detail && (_jsx("span", { className: "shrink-0 truncate text-[10px] text-muted-foreground", children: ref.detail }))] }, `${g.id}:${ref.token}`))) }, g.id)))] })] }) })] }))] }));
|
|
95
|
+
}
|
|
@@ -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
|
+
};
|