@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.
Files changed (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. 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(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) => {
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: `h-8 flex-1 text-xs${col.kind === 'expression' ? ' font-mono' : ''}` }))] }, 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] })] }));
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 { toFieldName } from '../previews/object-fields-io';
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 = toFieldName(v);
37
+ patch.name = slugify(v);
37
38
  onPatch(patch);
38
39
  };
39
40
  const setName = (v) => {
40
41
  nameTouched.current = true;
41
- onPatch({ name: toFieldName(v) });
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, toFieldName, indexOfField, } from '../previews/object-fields-io';
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 = toFieldName(rawNext);
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 = [...options];
261
+ const next = [...rows];
228
262
  next[i] = { ...next[i], ...patch };
229
- onChange(next);
263
+ commit(next);
230
264
  };
231
- const remove = (i) => onChange(options.filter((_, j) => j !== i));
232
- const move = (i, to) => onChange(moveArray(options, i, to));
233
- const add = () => onChange([...options, { value: '', label: '' }]);
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: options.length })] }), options.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: options.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 === options.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)] }))] }));
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 ? 'Select a field' : 'Set the target object first';
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: "Picker config" }), _jsx(InspectorComboField, { label: "Display field", value: displayField, onCommit: (v) => patchDef({ displayField: v || undefined }), options: fieldOptions, loading: loading, placeholder: fieldPlaceholder, searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), _jsx(InspectorComboField, { label: "Description field", value: descriptionField, onCommit: (v) => patchDef({ descriptionField: v || undefined }), options: fieldOptions, loading: loading, placeholder: fieldPlaceholder, searchPlaceholder: "Search fields\u2026", 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: "Selectable records" }), _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" }), " Add filter"] }))] }), filters.length === 0 ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-center text-[11px] text-muted-foreground", children: ["No filter \u2014 every ", reference || 'related', " record is selectable."] })) : (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: [_jsxs("span", { className: "text-[11px] text-muted-foreground", children: ["Filter ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove filter", 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: "Field", value: f.field ?? '', onCommit: (v) => patchFilter(i, { field: v }), options: fieldOptions, loading: loading, placeholder: fieldPlaceholder, searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: "Operator", value: f.operator ?? 'eq', options: LOOKUP_OPERATORS, onCommit: (v) => patchFilter(i, { operator: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: "Value", 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: "Depends on (same-record fields)" }), 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: "Add a field this lookup depends on\u2026", searchPlaceholder: "Search this object's fields\u2026", disabled: readOnly, allowCustom: false, mono: true }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 pt-1", children: [_jsx(InspectorNumberField, { label: "Picker page size", value: pageSize, onCommit: (v) => patchDef({ lookupPageSize: v }), placeholder: "10", disabled: readOnly }), _jsx("div", { className: "flex items-end pb-1.5", children: _jsx(InspectorCheckboxField, { label: "Allow quick-create", value: allowCreate, onCommit: (v) => patchDef({ allowCreate: v || undefined }), disabled: readOnly }) })] })] }));
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: [_jsx(InspectorTextField, { label: tr('engine.inspector.report.label'), value: labelValue, onCommit: (v) => onPatch({ label: v }), 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 }) }))] })), _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'], readOnly: readOnly, onChange: (next) => onPatch(next) })) : (_jsx("p", { className: "text-[11px] text-muted-foreground", children: tr('engine.inspector.report.noSchema') })) })] }));
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
+ };