@object-ui/app-shell 11.3.0 → 11.5.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 (90) hide show
  1. package/CHANGELOG.md +700 -0
  2. package/README.md +32 -0
  3. package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
  4. package/dist/console/AppContent.js +145 -26
  5. package/dist/console/ConsoleShell.js +25 -3
  6. package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
  7. package/dist/console/home/CloudOnboardingNext.js +14 -4
  8. package/dist/console/home/HomePage.js +34 -7
  9. package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
  10. package/dist/console/organizations/OrganizationsPage.js +16 -7
  11. package/dist/context/CommandPaletteProvider.js +2 -1
  12. package/dist/hooks/useConsoleActionRuntime.js +32 -3
  13. package/dist/hooks/useObjectActions.js +16 -4
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +3 -0
  16. package/dist/layout/AppHeader.js +13 -5
  17. package/dist/layout/AppSidebar.js +10 -4
  18. package/dist/observability/sentry.d.ts +5 -0
  19. package/dist/observability/sentry.js +6 -1
  20. package/dist/preview/DraftChangesPanel.d.ts +31 -1
  21. package/dist/preview/DraftChangesPanel.js +146 -18
  22. package/dist/urlParams.d.ts +68 -0
  23. package/dist/urlParams.js +76 -0
  24. package/dist/utils/appRoute.d.ts +15 -0
  25. package/dist/utils/appRoute.js +22 -0
  26. package/dist/utils/deriveRelatedLists.d.ts +20 -5
  27. package/dist/utils/deriveRelatedLists.js +31 -13
  28. package/dist/utils/index.d.ts +1 -1
  29. package/dist/utils/index.js +1 -1
  30. package/dist/utils/pageTabsUrlSync.d.ts +32 -0
  31. package/dist/utils/pageTabsUrlSync.js +43 -0
  32. package/dist/utils/recordFormNavigation.d.ts +40 -0
  33. package/dist/utils/recordFormNavigation.js +30 -0
  34. package/dist/utils/resolveViewId.d.ts +23 -0
  35. package/dist/utils/resolveViewId.js +37 -0
  36. package/dist/utils/warnSuppressedListNav.d.ts +10 -0
  37. package/dist/utils/warnSuppressedListNav.js +40 -0
  38. package/dist/views/InterfaceListPage.d.ts +1 -0
  39. package/dist/views/InterfaceListPage.js +7 -5
  40. package/dist/views/ObjectDataPage.d.ts +29 -0
  41. package/dist/views/ObjectDataPage.js +227 -0
  42. package/dist/views/ObjectView.js +65 -13
  43. package/dist/views/RecordDetailView.js +191 -123
  44. package/dist/views/RecordFormPage.js +7 -1
  45. package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
  46. package/dist/views/RelatedRecordActionsBridge.js +147 -0
  47. package/dist/views/metadata-admin/PackagesPage.js +18 -7
  48. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
  49. package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
  50. package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
  51. package/dist/views/metadata-admin/i18n.d.ts +12 -21
  52. package/dist/views/metadata-admin/i18n.js +557 -6
  53. package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
  54. package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
  55. package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
  56. package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
  57. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +60 -30
  58. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
  59. package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
  60. package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
  61. package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
  62. package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
  63. package/dist/views/metadata-admin/nav-selection.js +81 -0
  64. package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
  65. package/dist/views/metadata-admin/permission-slice.js +70 -0
  66. package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
  67. package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
  68. package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
  69. package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
  70. package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
  71. package/dist/views/studio-design/BuilderLanding.js +126 -0
  72. package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
  73. package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
  74. package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
  75. package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
  76. package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
  77. package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
  78. package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
  79. package/dist/views/studio-design/PackageIdInput.js +40 -0
  80. package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
  81. package/dist/views/studio-design/StudioDesignSurface.js +962 -145
  82. package/dist/views/studio-design/metadataError.d.ts +23 -0
  83. package/dist/views/studio-design/metadataError.js +44 -0
  84. package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
  85. package/dist/views/studio-design/packageSurfaces.js +34 -0
  86. package/dist/views/studio-design/packages-io.d.ts +38 -0
  87. package/dist/views/studio-design/packages-io.js +73 -0
  88. package/dist/views/studio-design/skeletons.d.ts +16 -0
  89. package/dist/views/studio-design/skeletons.js +51 -0
  90. package/package.json +42 -39
@@ -6,11 +6,18 @@
6
6
  * `<rootKey>[i]` for top-level items, `<rootKey>[i].children[j]` for
7
7
  * nested. This inspector walks the same path to read/write.
8
8
  *
9
- * For simplicity v1 only edits the leaf node (label / path / icon /
10
- * kind). Reparenting and re-ordering happen elsewhere (drag handles,
11
- * the top-level SchemaForm).
9
+ * Target editing is contract-first (#2245): a `type` selector plus
10
+ * per-type typed-target pickers replacing the old off-spec `path` /
11
+ * `kind` free-text fields the runtime never read. `object` items expose
12
+ * the four landing modes matching resolveHref precedence (default /
13
+ * named view / record deep-link / filters slice on `/data`, ADR-0055);
14
+ * the mode is derived from field presence — never persisted — and every
15
+ * edit clears the other modes' fields plus all legacy keys, so touching
16
+ * a legacy item migrates it to spec shape. A live href preview renders
17
+ * the REAL runtime landing via resolveHref, making "declared =
18
+ * enforced" visible while editing.
12
19
  */
13
20
  import * as React from 'react';
14
21
  import type { MetadataInspectorProps } from '../inspector-registry';
15
- export declare function AppNavInspector({ selection, draft, onPatch, onClearSelection, onSelectionChange, locale, readOnly }: MetadataInspectorProps): React.JSX.Element;
22
+ export declare function AppNavInspector({ selection, draft, name, onPatch, onClearSelection, onSelectionChange, locale, readOnly }: MetadataInspectorProps): React.JSX.Element;
16
23
  export declare const APP_NAV_ROOT_KEYS: string[];
@@ -1,8 +1,34 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
+ /**
4
+ * AppNavInspector — scoped editor for the selected app navigation item.
5
+ *
6
+ * Apps accept nav under several keys (nav / navigation / tabs / items
7
+ * / menu). The preview emits selection id as a dotted path
8
+ * `<rootKey>[i]` for top-level items, `<rootKey>[i].children[j]` for
9
+ * nested. This inspector walks the same path to read/write.
10
+ *
11
+ * Target editing is contract-first (#2245): a `type` selector plus
12
+ * per-type typed-target pickers replacing the old off-spec `path` /
13
+ * `kind` free-text fields the runtime never read. `object` items expose
14
+ * the four landing modes matching resolveHref precedence (default /
15
+ * named view / record deep-link / filters slice on `/data`, ADR-0055);
16
+ * the mode is derived from field presence — never persisted — and every
17
+ * edit clears the other modes' fields plus all legacy keys, so touching
18
+ * a legacy item migrates it to spec shape. A live href preview renders
19
+ * the REAL runtime landing via resolveHref, making "declared =
20
+ * enforced" visible while editing.
21
+ */
22
+ import * as React from 'react';
23
+ import { resolveHref } from '@object-ui/layout';
24
+ import { Plus, X } from 'lucide-react';
2
25
  import { t } from '../i18n';
26
+ import { useMetadataClient } from '../useMetadata';
27
+ import { useObjectFields } from '../previews/useObjectFields';
3
28
  import { InspectorShell, InspectorReorderButtons, InspectorTextField, InspectorSelectField, InspectorRemoveButton, InspectorEmptyState, spliceArray, moveArray, } from './_shared';
29
+ import { InspectorComboField } from './InspectorComboField';
30
+ import { NAV_ITEM_TYPES, NAV_TYPE_TARGETS, OBJECT_TARGET_MODES, OBJECT_MODE_FIELDS, inferNavItemType, deriveObjectTargetMode, clearedTargetPatch, ensureNavId, } from './nav-target';
4
31
  const ROOT_KEYS = ['nav', 'navigation', 'tabs', 'items', 'menu'];
5
- const KINDS = ['object', 'page', 'dashboard', 'report', 'link', 'group'];
6
32
  /** Parse "nav[0].children[2]" → [{key:'nav', index:0}, {key:'children', index:2}]. */
7
33
  function parsePath(id) {
8
34
  const segs = id.split('.');
@@ -82,16 +108,111 @@ function writeSiblings(draft, hops, nextSiblings) {
82
108
  arr[parentHop.index] = parentCopy;
83
109
  return { [rootKey]: root };
84
110
  }
85
- export function AppNavInspector({ selection, draft, onPatch, onClearSelection, onSelectionChange, locale, readOnly }) {
111
+ /** Fetch a metadata type's items as combobox options (name → label (name)). */
112
+ function useMetadataOptions(type) {
113
+ const client = useMetadataClient();
114
+ const [state, setState] = React.useState({
115
+ options: [],
116
+ loading: !!type,
117
+ });
118
+ React.useEffect(() => {
119
+ if (!type) {
120
+ setState({ options: [], loading: false });
121
+ return;
122
+ }
123
+ let cancelled = false;
124
+ setState((s) => ({ ...s, loading: true }));
125
+ client
126
+ .list(type)
127
+ .then((rows) => {
128
+ if (cancelled)
129
+ return;
130
+ const options = (Array.isArray(rows) ? rows : [])
131
+ .filter((r) => r && typeof r.name === 'string' && r.name)
132
+ .map((r) => ({
133
+ value: String(r.name),
134
+ label: typeof r.label === 'string' && r.label && r.label !== r.name ? `${r.label} (${r.name})` : String(r.name),
135
+ }));
136
+ setState({ options, loading: false });
137
+ })
138
+ .catch(() => {
139
+ if (!cancelled)
140
+ setState({ options: [], loading: false });
141
+ });
142
+ return () => {
143
+ cancelled = true;
144
+ };
145
+ }, [client, type]);
146
+ return state;
147
+ }
148
+ /** Preview template context that keeps template vars VISIBLE in the href. */
149
+ const PREVIEW_CTX = {
150
+ currentUserId: '{current_user_id}',
151
+ currentOrgId: '{current_org_id}',
152
+ };
153
+ /** Key/value rows editor for the `filters` landing mode (ADR-0055). */
154
+ function FiltersEditor({ objectName, filters, onCommit, disabled, locale, }) {
155
+ const { fields } = useObjectFields(objectName || undefined);
156
+ const fieldOptions = React.useMemo(() => fields.map((f) => ({ value: f.name, label: f.label && f.label !== f.name ? `${f.label} (${f.name})` : f.name })), [fields]);
157
+ const entries = Object.entries(filters);
158
+ const update = (index, key, value) => {
159
+ const next = entries.slice();
160
+ next[index] = [key, value];
161
+ onCommit(Object.fromEntries(next.filter(([k]) => k !== '')));
162
+ };
163
+ const remove = (index) => {
164
+ const next = entries.slice();
165
+ next.splice(index, 1);
166
+ onCommit(Object.fromEntries(next));
167
+ };
168
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsx("div", { className: "text-[11px] font-medium text-muted-foreground", children: t('engine.inspector.appNav.filters', locale) }), entries.map(([field, value], i) => (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(InspectorComboField, { value: field, onCommit: (v) => update(i, v, value), options: fieldOptions, placeholder: t('engine.inspector.appNav.filtersField', locale), disabled: disabled, mono: true }) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "=" }), _jsx("input", { value: value, onChange: (e) => update(i, field, e.target.value), placeholder: t('engine.inspector.appNav.filtersValue', locale), disabled: disabled, className: "h-7 w-0 min-w-0 flex-1 rounded border bg-background px-2 font-mono text-xs", "data-testid": `nav-filter-value-${field || 'new'}` }), _jsx("button", { type: "button", onClick: () => remove(i), disabled: disabled, className: "rounded p-1 text-muted-foreground hover:bg-muted", "aria-label": t('engine.inspector.appNav.filtersRemove', locale), "data-testid": `nav-filter-remove-${field || 'new'}`, children: _jsx(X, { className: "h-3 w-3" }) })] }, `${field}-${i}`))), _jsxs("button", { type: "button", onClick: () => onCommit({ ...filters, '': '' }), disabled: disabled || Object.prototype.hasOwnProperty.call(filters, ''), className: "inline-flex items-center gap-1 rounded border px-2 py-1 text-[11px] text-muted-foreground hover:bg-muted", "data-testid": "nav-filter-add", children: [_jsx(Plus, { className: "h-3 w-3" }), t('engine.inspector.appNav.filtersAdd', locale)] }), _jsx("p", { className: "text-[11px] text-muted-foreground", children: t('engine.inspector.appNav.filtersHint', locale) })] }));
169
+ }
170
+ export function AppNavInspector({ selection, draft, name, onPatch, onClearSelection, onSelectionChange, locale, readOnly }) {
86
171
  const hops = parsePath(selection.id);
87
172
  const { node, parent, index } = hops ? readAt(draft, hops) : { node: null, parent: [], index: -1 };
173
+ const navType = node ? inferNavItemType(node) : null;
174
+ const objectMode = node ? deriveObjectTargetMode(node) : 'default';
175
+ const objectName = String(node?.objectName ?? node?.object ?? '');
176
+ // Hooks run unconditionally (before the not-found early return) to keep
177
+ // the Rules of Hooks satisfied; `undefined` type disables the fetch.
178
+ const objectOptions = useMetadataOptions(navType === 'object' ? 'object' : undefined);
179
+ const targetMeta = navType && navType !== 'object' ? NAV_TYPE_TARGETS[navType].metaType : undefined;
180
+ const targetOptions = useMetadataOptions(targetMeta);
181
+ const viewOptionsRaw = useMetadataOptions(navType === 'object' && objectMode === 'view' ? 'view' : undefined);
182
+ // Views are named `<object>.<key>` (MetadataProvider) — scope the picker
183
+ // to the bound object instead of offering the whole workspace's views.
184
+ const viewOptions = React.useMemo(() => objectName
185
+ ? viewOptionsRaw.options.filter((o) => o.value.startsWith(`${objectName}.`) || !o.value.includes('.'))
186
+ : viewOptionsRaw.options, [viewOptionsRaw.options, objectName]);
88
187
  if (!hops || !node) {
89
188
  return (_jsx(InspectorShell, { kindLabel: t('engine.inspector.appNav.kind', locale), title: selection.label ?? selection.id, onClose: onClearSelection, closeLabel: t('engine.inspector.appNav.close', locale), children: _jsx(InspectorEmptyState, { message: selection.id }) }));
90
189
  }
91
- const labelOf = node.label ?? node.title ?? node.name ?? node.path ?? selection.id;
92
- const path = node.path ?? node.href ?? node.route ?? '';
190
+ const labelOf = node.label ?? node.title ?? node.name ?? selection.id;
191
+ /**
192
+ * Every write enforces the spec invariants: snake_case `id`, an explicit
193
+ * `type`, and the caller-provided field clears — editing a legacy item
194
+ * IS its migration to spec shape.
195
+ */
93
196
  const patch = (updates) => {
94
- onPatch(writeAt(draft, hops, { ...node, ...updates }));
197
+ const nextType = (updates.type ?? node.type ?? navType ?? undefined);
198
+ onPatch(writeAt(draft, hops, {
199
+ ...node,
200
+ id: ensureNavId(node, parent),
201
+ ...(nextType ? { type: nextType } : {}),
202
+ ...updates,
203
+ }));
204
+ };
205
+ const switchType = (nextType) => {
206
+ const targetKey = NAV_TYPE_TARGETS[nextType].targetKey;
207
+ const keep = nextType === 'object' ? OBJECT_MODE_FIELDS.default : targetKey ? [targetKey] : [];
208
+ patch({ type: nextType, ...clearedTargetPatch(keep) });
209
+ };
210
+ const switchObjectMode = (mode) => {
211
+ const cleared = clearedTargetPatch(OBJECT_MODE_FIELDS[mode]);
212
+ if (mode === 'filters')
213
+ patch({ ...cleared, objectName, filters: node.filters ?? {} });
214
+ else
215
+ patch({ ...cleared, objectName });
95
216
  };
96
217
  const remove = () => {
97
218
  onPatch(writeAt(draft, hops, null));
@@ -105,6 +226,19 @@ export function AppNavInspector({ selection, draft, onPatch, onClearSelection, o
105
226
  const newId = prefix ? `${prefix}.${leafKey}[${to}]` : `${leafKey}[${to}]`;
106
227
  onSelectionChange?.({ kind: 'nav', id: newId, label: String(labelOf) });
107
228
  };
108
- return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.appNav.kind', locale), title: String(labelOf), onClose: onClearSelection, closeLabel: t('engine.inspector.appNav.close', locale), headerActions: _jsx(InspectorReorderButtons, { index: index, total: parent.length, onMove: move, upLabel: t('engine.inspector.reorder.up', locale), downLabel: t('engine.inspector.reorder.down', locale), disabled: readOnly }), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.appNav.remove', locale), onClick: remove, disabled: readOnly }), children: [_jsx(InspectorTextField, { label: t('engine.inspector.appNav.label', locale), value: String(node.label ?? node.title ?? node.name ?? ''), onCommit: (v) => patch({ label: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: t('engine.inspector.appNav.path', locale), value: String(path), onCommit: (v) => patch({ path: v }), disabled: readOnly, mono: true }), _jsx(InspectorTextField, { label: t('engine.inspector.appNav.icon', locale), value: String(node.icon ?? ''), onCommit: (v) => patch({ icon: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: t('engine.inspector.appNav.kindField', locale), value: node.kind, options: KINDS.map((v) => ({ value: v, label: v })), onCommit: (v) => patch({ kind: v }), disabled: readOnly })] }));
229
+ // Live landing preview the REAL runtime URL for the current config
230
+ // (resolveHref is the single source of truth), template vars kept
231
+ // visible. `#` reads as "no target yet".
232
+ const previewHref = navType && navType !== 'group'
233
+ ? resolveHref({ ...node, type: navType }, `/apps/${name || 'app'}`, PREVIEW_CTX).href
234
+ : null;
235
+ const target = NAV_TYPE_TARGETS[navType ?? 'group'];
236
+ return (_jsxs(InspectorShell, { kindLabel: t('engine.inspector.appNav.kind', locale), title: String(labelOf), onClose: onClearSelection, closeLabel: t('engine.inspector.appNav.close', locale), headerActions: _jsx(InspectorReorderButtons, { index: index, total: parent.length, onMove: move, upLabel: t('engine.inspector.reorder.up', locale), downLabel: t('engine.inspector.reorder.down', locale), disabled: readOnly }), footer: _jsx(InspectorRemoveButton, { label: t('engine.inspector.appNav.remove', locale), onClick: remove, disabled: readOnly }), children: [_jsx(InspectorTextField, { label: t('engine.inspector.appNav.label', locale), value: String(node.label ?? node.title ?? node.name ?? ''), onCommit: (v) => patch({ label: v }), disabled: readOnly }), _jsx(InspectorTextField, { label: t('engine.inspector.appNav.icon', locale), value: String(node.icon ?? ''), onCommit: (v) => patch({ icon: v }), disabled: readOnly }), _jsx(InspectorSelectField, { label: t('engine.inspector.appNav.typeField', locale), value: navType ?? '', options: NAV_ITEM_TYPES.map((v) => ({ value: v, label: t(`engine.inspector.appNav.type.${v}`, locale) })), onCommit: (v) => switchType(v), disabled: readOnly }), navType === 'object' && (_jsxs(_Fragment, { children: [_jsx(InspectorComboField, { label: t('engine.inspector.appNav.object', locale), value: objectName, options: objectOptions.options, loading: objectOptions.loading, onCommit: (v) => patch({ objectName: v, object: undefined }), disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: t('engine.inspector.appNav.targetMode', locale), value: objectMode, options: OBJECT_TARGET_MODES.map((m) => ({ value: m, label: t(`engine.inspector.appNav.mode.${m}`, locale) })), onCommit: (v) => switchObjectMode(v), disabled: readOnly || !objectName }), objectMode === 'view' && (_jsx(InspectorComboField, { label: t('engine.inspector.appNav.view', locale), value: String(node.viewName ?? ''), options: viewOptions, loading: viewOptionsRaw.loading, onCommit: (v) => patch({ viewName: v }), disabled: readOnly, mono: true })), objectMode === 'record' && (_jsxs(_Fragment, { children: [_jsx(InspectorTextField, { label: t('engine.inspector.appNav.recordId', locale), value: String(node.recordId ?? ''), onCommit: (v) => patch({ recordId: v }), disabled: readOnly, mono: true }), _jsx("p", { className: "text-[11px] text-muted-foreground", children: t('engine.inspector.appNav.recordIdHint', locale) }), _jsx(InspectorSelectField, { label: t('engine.inspector.appNav.recordMode', locale), value: String(node.recordMode ?? 'view'), options: [
237
+ { value: 'view', label: t('engine.inspector.appNav.recordModeView', locale) },
238
+ { value: 'edit', label: t('engine.inspector.appNav.recordModeEdit', locale) },
239
+ ], onCommit: (v) => patch({ recordMode: v }), disabled: readOnly })] })), objectMode === 'filters' && (_jsx(FiltersEditor, { objectName: objectName, filters: node.filters ?? {}, onCommit: (next) => patch({ filters: next }), disabled: readOnly, locale: locale }))] })), navType && navType !== 'object' && navType !== 'group' && target.targetKey && (target.metaType ? (_jsx(InspectorComboField, { label: t(`engine.inspector.appNav.type.${navType}`, locale), value: String(node[target.targetKey] ?? ''), options: targetOptions.options, loading: targetOptions.loading, onCommit: (v) => patch({ [target.targetKey]: v }), disabled: readOnly, mono: true })) : (_jsxs(_Fragment, { children: [_jsx(InspectorTextField, { label: t('engine.inspector.appNav.url', locale), value: String(node.url ?? node.href ?? ''), onCommit: (v) => patch({ url: v, href: undefined }), disabled: readOnly, mono: true }), _jsx(InspectorSelectField, { label: t('engine.inspector.appNav.urlTarget', locale), value: String(node.target ?? '_self'), options: [
240
+ { value: '_self', label: t('engine.inspector.appNav.urlTargetSelf', locale) },
241
+ { value: '_blank', label: t('engine.inspector.appNav.urlTargetBlank', locale) },
242
+ ], onCommit: (v) => patch({ target: v }), disabled: readOnly })] }))), previewHref && (_jsxs("div", { className: "rounded border bg-muted/30 px-2 py-1.5", "data-testid": "nav-target-preview", children: [_jsx("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.inspector.appNav.preview', locale) }), _jsx("div", { className: "break-all font-mono text-[11px]", children: previewHref })] }))] }));
109
243
  }
110
244
  export const APP_NAV_ROOT_KEYS = ROOT_KEYS;
@@ -33,10 +33,15 @@ export interface FlowReferenceContext {
33
33
  /** The node currently being edited — used to resolve sibling config keys. */
34
34
  node: Record<string, unknown> | null;
35
35
  }
36
+ interface Option {
37
+ value: string;
38
+ label: string;
39
+ }
36
40
  /** A concrete (non-polymorphic) reference resolution. */
37
41
  export interface ResolvedRef {
38
42
  kind: ReferenceKind;
39
43
  objectSource?: string;
44
+ connectorSource?: string;
40
45
  }
41
46
  /**
42
47
  * Resolve a (possibly polymorphic) reference spec to a concrete kind. For a
@@ -45,6 +50,14 @@ export interface ResolvedRef {
45
50
  * the caller then renders plain free text.
46
51
  */
47
52
  export declare function resolveRefKind(ref: FlowReferenceSpec | undefined, sibling: (key: string) => unknown): ResolvedRef | undefined;
53
+ /**
54
+ * Resolve the chosen connector name for a `connector-action` reference — read
55
+ * from the sibling key on this node's `connectorConfig` block (default
56
+ * `connectorId`), which is where the connector picker writes it.
57
+ */
58
+ export declare function resolveConnectorName(kind: ReferenceKind, connectorSource: string | undefined, ctx: FlowReferenceContext): string | undefined;
59
+ /** A connector descriptor's action list → combobox options (exported for test). */
60
+ export declare function connectorActionsToOptions(actions: unknown): Option[];
48
61
  export interface ReferenceComboboxProps {
49
62
  /** The resolved concrete reference, or undefined → plain free text. */
50
63
  resolved: ResolvedRef | undefined;
@@ -81,3 +94,4 @@ export interface FlowReferenceFieldProps {
81
94
  * `template` follows `actionType`).
82
95
  */
83
96
  export declare function FlowReferenceField({ field, value, onCommit, disabled, context }: FlowReferenceFieldProps): React.JSX.Element;
97
+ export {};
@@ -56,12 +56,12 @@ export function resolveRefKind(ref, sibling) {
56
56
  if (!ref)
57
57
  return undefined;
58
58
  if (ref.kind)
59
- return { kind: ref.kind, objectSource: ref.objectSource };
59
+ return { kind: ref.kind, objectSource: ref.objectSource, connectorSource: ref.connectorSource };
60
60
  if (ref.kindFrom && ref.map) {
61
61
  const disc = sibling(ref.kindFrom);
62
62
  const k = typeof disc === 'string' ? ref.map[disc] : undefined;
63
63
  if (k)
64
- return { kind: k, objectSource: ref.objectSource };
64
+ return { kind: k, objectSource: ref.objectSource, connectorSource: ref.connectorSource };
65
65
  }
66
66
  return undefined;
67
67
  }
@@ -86,6 +86,31 @@ function resolveObjectName(kind, objectSource, ctx) {
86
86
  // A sibling config key on the same node (CRUD nodes carry their own objectName).
87
87
  return configString(ctx.node, src);
88
88
  }
89
+ /**
90
+ * Resolve the chosen connector name for a `connector-action` reference — read
91
+ * from the sibling key on this node's `connectorConfig` block (default
92
+ * `connectorId`), which is where the connector picker writes it.
93
+ */
94
+ export function resolveConnectorName(kind, connectorSource, ctx) {
95
+ if (kind !== 'connector-action')
96
+ return undefined;
97
+ const cc = ctx.node?.connectorConfig;
98
+ if (!cc || typeof cc !== 'object' || Array.isArray(cc))
99
+ return undefined;
100
+ const v = cc[connectorSource || 'connectorId'];
101
+ return typeof v === 'string' && v ? v : undefined;
102
+ }
103
+ /** A connector descriptor's action list → combobox options (exported for test). */
104
+ export function connectorActionsToOptions(actions) {
105
+ if (!Array.isArray(actions))
106
+ return [];
107
+ return actions
108
+ .filter((a) => !!a && typeof a.key === 'string' && !!a.key)
109
+ .map((a) => ({
110
+ value: a.key,
111
+ label: typeof a.label === 'string' && a.label && a.label !== a.key ? `${a.label} (${a.key})` : a.key,
112
+ }));
113
+ }
89
114
  /**
90
115
  * Fetch a metadata type's items as combobox options. `type === undefined`
91
116
  * disables the fetch (returns empty), so the hook can be called
@@ -127,6 +152,45 @@ function useMetadataListOptions(type) {
127
152
  }, [client, type]);
128
153
  return state;
129
154
  }
155
+ /**
156
+ * Fetch a connector's actions as combobox options from the runtime connector
157
+ * descriptors (`GET /api/v1/automation/connectors`, each `{ name, actions:
158
+ * [{key,label}] }`). `connectorName === undefined` disables the fetch (so the
159
+ * hook is safe to call unconditionally). Degrades to empty on any failure.
160
+ */
161
+ function useConnectorActionOptions(connectorName) {
162
+ const [state, setState] = React.useState({
163
+ options: [],
164
+ loading: !!connectorName,
165
+ });
166
+ React.useEffect(() => {
167
+ if (!connectorName) {
168
+ setState({ options: [], loading: false });
169
+ return;
170
+ }
171
+ let cancelled = false;
172
+ setState((s) => ({ ...s, loading: true }));
173
+ fetch('/api/v1/automation/connectors', { credentials: 'include', headers: { Accept: 'application/json' } })
174
+ .then((r) => (r.ok ? r.json() : null))
175
+ .then((payload) => {
176
+ if (cancelled)
177
+ return;
178
+ const connectors = payload?.data?.connectors ?? payload?.connectors ?? [];
179
+ const conn = Array.isArray(connectors)
180
+ ? connectors.find((c) => c?.name === connectorName)
181
+ : undefined;
182
+ setState({ options: connectorActionsToOptions(conn?.actions), loading: false });
183
+ })
184
+ .catch(() => {
185
+ if (!cancelled)
186
+ setState({ options: [], loading: false });
187
+ });
188
+ return () => {
189
+ cancelled = true;
190
+ };
191
+ }, [connectorName]);
192
+ return state;
193
+ }
130
194
  /**
131
195
  * The bare reference combobox — suggestions for `resolved.kind`, always
132
196
  * free-text editable. Hooks are called unconditionally (kind-gated args) so the
@@ -139,8 +203,11 @@ export function ReferenceCombobox({ resolved, value, onCommit, onBlur, disabled,
139
203
  // object-field: resolve the target object, then its field catalog.
140
204
  const objectName = resolved ? resolveObjectName(resolved.kind, resolved.objectSource, ctx) : undefined;
141
205
  const { fields: objectFields } = useObjectFields(kind === 'object-field' ? objectName : undefined);
206
+ // connector-action: resolve the chosen connector, then its action catalog.
207
+ const connectorName = resolved ? resolveConnectorName(resolved.kind, resolved.connectorSource, ctx) : undefined;
208
+ const { options: connectorActionOptions } = useConnectorActionOptions(kind === 'connector-action' ? connectorName : undefined);
142
209
  // Flat metadata-list kinds (object / flow / role / user / team / …).
143
- const listType = kind && kind !== 'object-field' && kind !== 'node' ? KIND_TO_META_TYPE[kind] : undefined;
210
+ const listType = kind && kind !== 'object-field' && kind !== 'node' && kind !== 'connector-action' ? KIND_TO_META_TYPE[kind] : undefined;
144
211
  const { options: listOptions } = useMetadataListOptions(listType);
145
212
  const options = React.useMemo(() => {
146
213
  if (kind === 'object-field') {
@@ -149,6 +216,8 @@ export function ReferenceCombobox({ resolved, value, onCommit, onBlur, disabled,
149
216
  label: f.label && f.label !== f.name ? `${f.label} (${f.name})` : f.name,
150
217
  }));
151
218
  }
219
+ if (kind === 'connector-action')
220
+ return connectorActionOptions;
152
221
  if (kind === 'node') {
153
222
  const nodes = Array.isArray(ctx.draft.nodes) ? ctx.draft.nodes : [];
154
223
  const currentId = typeof ctx.node?.id === 'string' ? ctx.node.id : undefined;
@@ -163,11 +232,13 @@ export function ReferenceCombobox({ resolved, value, onCommit, onBlur, disabled,
163
232
  if (listType)
164
233
  return listOptions;
165
234
  return [];
166
- }, [kind, listType, objectFields, listOptions, ctx.draft, ctx.node]);
235
+ }, [kind, listType, objectFields, connectorActionOptions, listOptions, ctx.draft, ctx.node]);
167
236
  // For an object-field whose object can't be resolved, tell the author why the
168
237
  // suggestions are empty — but still let them type a value.
169
238
  const unresolvedObject = kind === 'object-field' && !objectName;
170
- return (_jsxs("div", { className: "w-full space-y-1", children: [_jsx(Input, { list: options.length ? listId : undefined, value: value != null ? String(value) : '', onChange: (e) => onCommit(e.target.value), onBlur: onBlur, placeholder: placeholder, disabled: disabled, className: "h-8 text-sm" }), options.length > 0 && (_jsx("datalist", { id: listId, children: options.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })), showHint && kind === 'object-field' && objectName && (_jsxs("p", { className: "text-[11px] leading-snug text-muted-foreground", children: ["Fields of ", objectName, "."] })), showHint && unresolvedObject && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Set the flow\u2019s trigger object (on the Start node) to list fields." }))] }));
239
+ // Same for a connector-action with no connector chosen yet.
240
+ const unresolvedConnector = kind === 'connector-action' && !connectorName;
241
+ return (_jsxs("div", { className: "w-full space-y-1", children: [_jsx(Input, { list: options.length ? listId : undefined, value: value != null ? String(value) : '', onChange: (e) => onCommit(e.target.value), onBlur: onBlur, placeholder: placeholder, disabled: disabled, className: "h-8 text-sm" }), options.length > 0 && (_jsx("datalist", { id: listId, children: options.map((o) => (_jsx("option", { value: o.value, children: o.label }, o.value))) })), showHint && kind === 'object-field' && objectName && (_jsxs("p", { className: "text-[11px] leading-snug text-muted-foreground", children: ["Fields of ", objectName, "."] })), showHint && unresolvedObject && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Set the flow\u2019s trigger object (on the Start node) to list fields." })), showHint && kind === 'connector-action' && connectorName && (_jsxs("p", { className: "text-[11px] leading-snug text-muted-foreground", children: ["Actions of ", connectorName, "."] })), showHint && unresolvedConnector && (_jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: "Choose a Connector above to list its actions." }))] }));
171
242
  }
172
243
  /**
173
244
  * Inspector field wrapper: a labelled reference combobox. A polymorphic ref is
@@ -21,7 +21,6 @@ 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';
25
24
  import { useMetadataClient } from '../useMetadata';
26
25
  import { InspectorShell, InspectorReorderButtons, InspectorTextField, InspectorNumberField, InspectorSelectField, InspectorCheckboxField, InspectorRemoveButton, InspectorEmptyState, moveArray, } from './_shared';
27
26
  import { Button, Input, Label, Badge } from '@object-ui/components';
@@ -128,28 +127,33 @@ export function ObjectFieldInspector({ selection, draft, onPatch, onClearSelecti
128
127
  writeView({ shape: view.shape, entries: nextEntries });
129
128
  onSelectionChange?.({ kind: 'field', id: nextName, label: String(def.label ?? nextName) });
130
129
  };
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) => {
130
+ // Derive the API name from the label live, per keystroke with
131
+ // toFieldNameLoose (prefix-stable, unlike slugify which trims trailing
132
+ // underscores and would fight mid-word typing) while the name is still
133
+ // an auto-generated default and the user hasn't customised it. Mirrors the
134
+ // object/app Name behaviour. toFieldNameLoose returns '' for non-Latin
135
+ // labels, in which case the unique default name is kept. Pure — the
136
+ // caller applies the label and (if any) the derived name in one write,
137
+ // since two separate writeView() calls from the same stale `entry` closure
138
+ // would have the second clobber the first.
139
+ const deriveNameFor = (label) => {
137
140
  if (readOnly)
138
- return;
141
+ return null;
139
142
  const base = type === 'select' ? 'status' : type;
140
143
  const isAutoName = entry.name === base ||
141
- (entry.name.startsWith(`${base}_`) && /^\d+$/.test(entry.name.slice(base.length + 1)));
144
+ (entry.name.startsWith(`${base}_`) && /^\d+$/.test(entry.name.slice(base.length + 1))) ||
145
+ // Freshly added fields are named by nextFieldName() as `field_<N>`
146
+ // (StudioDesignSurface.tsx), independent of the field's type — match
147
+ // that scheme too, or a type-typed rename right after add never derives.
148
+ /^field_\d+$/.test(entry.name);
142
149
  if (!isAutoName)
143
- return;
144
- const derived = slugify(label);
150
+ return null;
151
+ const derived = toFieldNameLoose(label);
145
152
  if (!derived || derived === entry.name)
146
- return;
153
+ return null;
147
154
  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) });
155
+ return null;
156
+ return derived;
153
157
  };
154
158
  const removeField = () => {
155
159
  const nextEntries = view.entries.filter((_, i) => i !== idx);
@@ -201,7 +205,19 @@ export function ObjectFieldInspector({ selection, draft, onPatch, onClearSelecti
201
205
  label: typeof def.label === 'string' ? def.label : entry.name,
202
206
  }), onClick: removeField, disabled: readOnly }));
203
207
  const typeMetaLabel = isZh(locale) ? typeMeta?.labelZh : typeMeta?.label;
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: [
208
+ 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) => {
209
+ const derivedName = deriveNameFor(v);
210
+ const nextEntries = [...view.entries];
211
+ nextEntries[idx] = {
212
+ ...entry,
213
+ name: derivedName ?? entry.name,
214
+ def: { ...def, label: v },
215
+ };
216
+ writeView({ shape: view.shape, entries: nextEntries });
217
+ if (derivedName) {
218
+ onSelectionChange?.({ kind: 'field', id: derivedName, label: v });
219
+ }
220
+ }, 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: [
205
221
  { value: '', label: tr('designer.field.noGroup') },
206
222
  ...fieldGroups
207
223
  .filter((g) => typeof g.key === 'string')
@@ -265,7 +281,7 @@ function OptionsEditor({ options, onChange, disabled, locale, }) {
265
281
  const remove = (i) => commit(rows.filter((_, j) => j !== i));
266
282
  const move = (i, to) => commit(moveArray(rows, i, to));
267
283
  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)] }))] }));
284
+ 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.5", children: rows.map((o, i) => (_jsxs("div", { className: "rounded-md border border-border/60 p-1.5 space-y-1", children: [_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 min-w-0 flex-1 text-xs font-mono" }), _jsx(Input, { value: o.label ?? '', onChange: (e) => update(i, { label: e.target.value }), placeholder: t('designer.field.optLabel', locale), disabled: disabled, className: "h-7 min-w-0 flex-1 text-xs" })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "color", value: o.color ?? '#cccccc', onChange: (e) => update(i, { color: e.target.value }), disabled: disabled, className: "h-6 w-6 rounded border bg-background cursor-pointer p-0.5", title: t('designer.field.optColor', locale) }), _jsx("span", { className: "flex-1" }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 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-6 w-6 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-6 w-6 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)] }))] }));
269
285
  }
270
286
  /* ─────────────── Lookup picker config (displayField / filters / dependent) ─────────────── */
271
287
  const LOOKUP_OPERATORS = [
@@ -340,19 +356,33 @@ function useObjectOptions() {
340
356
  const [opts, setOpts] = React.useState([]);
341
357
  React.useEffect(() => {
342
358
  let cancelled = false;
343
- client
344
- .list('object')
345
- .then((items) => {
359
+ Promise.all([
360
+ client.list('object'),
361
+ // Draft objects are not yet published, so `list('object')` can't see
362
+ // them. Include them so a lookup can target a SIBLING object being
363
+ // designed in the same authoring pass (before the package's first
364
+ // publish) instead of forcing the author to type an API name blind.
365
+ client.listDrafts({ type: 'object' }).catch(() => []),
366
+ ])
367
+ .then(([published, drafts]) => {
346
368
  if (cancelled)
347
369
  return;
348
- const mapped = items
349
- .filter((i) => typeof i?.name === 'string' && i.name)
350
- .map((i) => ({
351
- value: i.name,
352
- label: i.label ? `${i.label} (${i.name})` : i.name,
353
- }))
354
- .sort((a, b) => a.value.localeCompare(b.value));
355
- setOpts(mapped);
370
+ const byName = new Map();
371
+ for (const i of published ?? []) {
372
+ if (typeof i?.name === 'string' && i.name && !byName.has(i.name)) {
373
+ byName.set(i.name, {
374
+ value: i.name,
375
+ label: i.label ? `${i.label} (${i.name})` : i.name,
376
+ });
377
+ }
378
+ }
379
+ for (const d of drafts ?? []) {
380
+ const name = d.name;
381
+ if (typeof name === 'string' && name && !byName.has(name)) {
382
+ byName.set(name, { value: name, label: `${name} (草稿)` });
383
+ }
384
+ }
385
+ setOpts([...byName.values()].sort((a, b) => a.value.localeCompare(b.value)));
356
386
  })
357
387
  .catch(() => {
358
388
  // Empty list — picker falls back to free-text. No banner needed.
@@ -42,7 +42,7 @@ export type FlowConfigFieldKind = 'text' | 'expression' | 'number' | 'boolean' |
42
42
  * Kinds that have no catalog in the current tenant simply degrade to a plain
43
43
  * text box — the control is always an editable combobox, never a hard dropdown.
44
44
  */
45
- export type ReferenceKind = 'object' | 'object-field' | 'flow' | 'role' | 'node' | 'user' | 'team' | 'queue' | 'department' | 'connector' | 'email-template';
45
+ export type ReferenceKind = 'object' | 'object-field' | 'flow' | 'role' | 'node' | 'user' | 'team' | 'queue' | 'department' | 'connector' | 'connector-action' | 'email-template';
46
46
  export interface FlowReferenceSpec {
47
47
  /**
48
48
  * Concrete reference kind. Omit when the kind is *polymorphic* — chosen at
@@ -57,6 +57,13 @@ export interface FlowReferenceSpec {
57
57
  * the object name (e.g. CRUD nodes resolve from their own `objectName`).
58
58
  */
59
59
  objectSource?: string;
60
+ /**
61
+ * For `connector-action` only: the sibling key (on this node's
62
+ * `connectorConfig` block) holding the chosen connector's name. Defaults to
63
+ * `'connectorId'`. The picker lists THAT connector's actions (from the runtime
64
+ * connector descriptors); with no connector chosen it degrades to free text.
65
+ */
66
+ connectorSource?: string;
60
67
  /**
61
68
  * Polymorphic reference: the kind is selected at render time by the value of
62
69
  * a sibling field/column named `kindFrom`, looked up in {@link map}. A value
@@ -347,9 +347,10 @@ const FLOW_NODE_CONFIG = {
347
347
  ],
348
348
  connector_action: [
349
349
  at('connectorConfig', 'connectorId', 'Connector', 'reference', { ref: { kind: 'connector' }, placeholder: 'slack · email · salesforce' }),
350
- // actionId is polymorphic on the chosen connector and has no flat catalog
350
+ // actionId is polymorphic on the chosen connector: the picker lists THAT
351
+ // connector's actions (runtime descriptors), degrading to free text if none.
351
352
  // (a deliberate open extension point) — stays free text.
352
- at('connectorConfig', 'actionId', 'Action', 'text', { placeholder: 'sendMessage · send' }),
353
+ at('connectorConfig', 'actionId', 'Action', 'reference', { ref: { kind: 'connector-action', connectorSource: 'connectorId' }, placeholder: 'sendMessage · send' }),
353
354
  at('connectorConfig', 'input', 'Input', 'keyValue', { help: 'Mapped inputs for the connector action.' }),
354
355
  { id: 'timeoutMs', path: ['timeoutMs'], label: 'Timeout (ms)', kind: 'number', placeholder: '30000' },
355
356
  ],