@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.
- package/CHANGELOG.md +700 -0
- package/README.md +32 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +2 -1
- package/dist/console/AppContent.js +145 -26
- package/dist/console/ConsoleShell.js +25 -3
- package/dist/console/home/CloudOnboardingNext.d.ts +9 -0
- package/dist/console/home/CloudOnboardingNext.js +14 -4
- package/dist/console/home/HomePage.js +34 -7
- package/dist/console/organizations/CreateWorkspaceDialog.js +33 -3
- package/dist/console/organizations/OrganizationsPage.js +16 -7
- package/dist/context/CommandPaletteProvider.js +2 -1
- package/dist/hooks/useConsoleActionRuntime.js +32 -3
- package/dist/hooks/useObjectActions.js +16 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/layout/AppHeader.js +13 -5
- package/dist/layout/AppSidebar.js +10 -4
- package/dist/observability/sentry.d.ts +5 -0
- package/dist/observability/sentry.js +6 -1
- package/dist/preview/DraftChangesPanel.d.ts +31 -1
- package/dist/preview/DraftChangesPanel.js +146 -18
- package/dist/urlParams.d.ts +68 -0
- package/dist/urlParams.js +76 -0
- package/dist/utils/appRoute.d.ts +15 -0
- package/dist/utils/appRoute.js +22 -0
- package/dist/utils/deriveRelatedLists.d.ts +20 -5
- package/dist/utils/deriveRelatedLists.js +31 -13
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/pageTabsUrlSync.d.ts +32 -0
- package/dist/utils/pageTabsUrlSync.js +43 -0
- package/dist/utils/recordFormNavigation.d.ts +40 -0
- package/dist/utils/recordFormNavigation.js +30 -0
- package/dist/utils/resolveViewId.d.ts +23 -0
- package/dist/utils/resolveViewId.js +37 -0
- package/dist/utils/warnSuppressedListNav.d.ts +10 -0
- package/dist/utils/warnSuppressedListNav.js +40 -0
- package/dist/views/InterfaceListPage.d.ts +1 -0
- package/dist/views/InterfaceListPage.js +7 -5
- package/dist/views/ObjectDataPage.d.ts +29 -0
- package/dist/views/ObjectDataPage.js +227 -0
- package/dist/views/ObjectView.js +65 -13
- package/dist/views/RecordDetailView.js +191 -123
- package/dist/views/RecordFormPage.js +7 -1
- package/dist/views/RelatedRecordActionsBridge.d.ts +33 -0
- package/dist/views/RelatedRecordActionsBridge.js +147 -0
- package/dist/views/metadata-admin/PackagesPage.js +18 -7
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +18 -1
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +73 -14
- package/dist/views/metadata-admin/ResourceEditPage.js +39 -0
- package/dist/views/metadata-admin/i18n.d.ts +12 -21
- package/dist/views/metadata-admin/i18n.js +557 -6
- package/dist/views/metadata-admin/inspectors/AppNavInspector.d.ts +11 -4
- package/dist/views/metadata-admin/inspectors/AppNavInspector.js +141 -7
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.d.ts +14 -0
- package/dist/views/metadata-admin/inspectors/FlowReferenceField.js +76 -5
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +60 -30
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +8 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +3 -2
- package/dist/views/metadata-admin/inspectors/nav-target.d.ts +52 -0
- package/dist/views/metadata-admin/inspectors/nav-target.js +149 -0
- package/dist/views/metadata-admin/nav-selection.d.ts +20 -0
- package/dist/views/metadata-admin/nav-selection.js +81 -0
- package/dist/views/metadata-admin/permission-slice.d.ts +66 -0
- package/dist/views/metadata-admin/permission-slice.js +70 -0
- package/dist/views/metadata-admin/previews/AppNavCanvas.js +19 -7
- package/dist/views/metadata-admin/previews/AppPreview.js +4 -2
- package/dist/views/metadata-admin/previews/FlowRunsPanel.d.ts +16 -7
- package/dist/views/metadata-admin/previews/FlowRunsPanel.js +18 -2
- package/dist/views/studio-design/BuilderLanding.d.ts +15 -0
- package/dist/views/studio-design/BuilderLanding.js +126 -0
- package/dist/views/studio-design/ObjectFormDesigner.d.ts +33 -0
- package/dist/views/studio-design/ObjectFormDesigner.js +231 -0
- package/dist/views/studio-design/ObjectSettingsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectSettingsPanel.js +46 -0
- package/dist/views/studio-design/ObjectValidationsPanel.d.ts +30 -0
- package/dist/views/studio-design/ObjectValidationsPanel.js +80 -0
- package/dist/views/studio-design/PackageIdInput.d.ts +31 -0
- package/dist/views/studio-design/PackageIdInput.js +40 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +13 -0
- package/dist/views/studio-design/StudioDesignSurface.js +962 -145
- package/dist/views/studio-design/metadataError.d.ts +23 -0
- package/dist/views/studio-design/metadataError.js +44 -0
- package/dist/views/studio-design/packageSurfaces.d.ts +49 -0
- package/dist/views/studio-design/packageSurfaces.js +34 -0
- package/dist/views/studio-design/packages-io.d.ts +38 -0
- package/dist/views/studio-design/packages-io.js +73 -0
- package/dist/views/studio-design/skeletons.d.ts +16 -0
- package/dist/views/studio-design/skeletons.js +51 -0
- 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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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 ??
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
344
|
-
.list('object')
|
|
345
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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', '
|
|
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
|
],
|