@object-ui/app-shell 7.0.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +281 -0
- package/dist/console/AppContent.js +14 -2
- package/dist/console/ai/AiChatPage.js +11 -7
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +25 -8
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.js +9 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
- package/dist/views/metadata-admin/ResourceListPage.js +8 -16
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/i18n.js +88 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +26 -0
- package/dist/views/metadata-admin/package-scope.js +43 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
- package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -24,6 +24,7 @@ import { useAssistant, requestAssistantReview, emitCanvasInvalidate } from '../a
|
|
|
24
24
|
import { fetchPendingDraftCount } from '../preview/draftStatus';
|
|
25
25
|
import { getRuntimeConfig } from '../runtime-config';
|
|
26
26
|
import { cloudPricingDeepLink } from '../console/marketplace/marketplaceApi';
|
|
27
|
+
import { shouldShowAgentPicker } from './agentPicker';
|
|
27
28
|
/**
|
|
28
29
|
* Display names for the two built-in platform agents (ADR-0063: `ask` / `build`,
|
|
29
30
|
* bound by surface). The backend ships English labels ("Assistant" / "Builder");
|
|
@@ -73,6 +74,9 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
73
74
|
toolRunning: '运行中',
|
|
74
75
|
toolAwaitingApproval: '等待确认',
|
|
75
76
|
toolFailed: '失败',
|
|
77
|
+
connectionWaiting: '正在等待服务器响应…',
|
|
78
|
+
connectionStalledLabel: '仍在处理中…',
|
|
79
|
+
connectionOfflineLabel: '网络已断开,正在重连…',
|
|
76
80
|
toolDetailsHidden: '已隐藏工具参数和原始结果,仅保留过程摘要。',
|
|
77
81
|
copy: '复制',
|
|
78
82
|
copied: '已复制',
|
|
@@ -99,6 +103,7 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
99
103
|
planApproveHint: '回复以确认或调整该方案。',
|
|
100
104
|
planApprove: '开始搭建',
|
|
101
105
|
planAdjust: '调整方案',
|
|
106
|
+
planBuilt: '已搭建',
|
|
102
107
|
planApproveMessage: '就按这个方案搭建吧。',
|
|
103
108
|
planApproveDefaultsMessage: '就按你的合理假设直接搭建,未决问题用默认即可。',
|
|
104
109
|
planAnswer: (question, option) => `关于「${question}」,我选择「${option}」。`,
|
|
@@ -128,6 +133,9 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
128
133
|
toolRunning: 'Running',
|
|
129
134
|
toolAwaitingApproval: 'Awaiting approval',
|
|
130
135
|
toolFailed: 'Failed',
|
|
136
|
+
connectionWaiting: 'Waiting for server…',
|
|
137
|
+
connectionStalledLabel: 'Still working…',
|
|
138
|
+
connectionOfflineLabel: 'Connection lost — reconnecting…',
|
|
131
139
|
toolDetailsHidden: 'Tool inputs and raw results are hidden in this view.',
|
|
132
140
|
copy: 'Copy',
|
|
133
141
|
copied: 'Copied',
|
|
@@ -154,6 +162,7 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
154
162
|
planApproveHint: 'Reply to approve or adjust this plan.',
|
|
155
163
|
planApprove: 'Build it',
|
|
156
164
|
planAdjust: 'Adjust',
|
|
165
|
+
planBuilt: 'Built',
|
|
157
166
|
planApproveMessage: 'Looks good — build it as proposed.',
|
|
158
167
|
planApproveDefaultsMessage: 'Build it with your best assumptions; use sensible defaults for the open questions.',
|
|
159
168
|
planAnswer: (question, option) => `For "${question}", go with: ${option}.`,
|
|
@@ -321,10 +330,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
321
330
|
sendMessage(prompt);
|
|
322
331
|
},
|
|
323
332
|
});
|
|
324
|
-
// Agent switcher —
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
333
|
+
// Agent switcher — Ask ↔ Build (plus any custom agents). Restrained by
|
|
334
|
+
// design: end users bound to a single agent never see it. `showAgentPicker`
|
|
335
|
+
// is true when AI development is unlocked (catalog serves both ask & build)
|
|
336
|
+
// or forced on; it still needs more than one agent to be a real choice.
|
|
328
337
|
const headerExtra = showAgentPicker && agents.length > 1 ? (_jsxs(Select, { value: activeAgent, onValueChange: onAgentChange, disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-[180px] text-xs", "data-testid": "floating-chatbot-agent-picker", children: _jsx(SelectValue, { placeholder: "Choose agent..." }) }), _jsx(SelectContent, { align: "end", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: agent.label }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[220px]", children: agent.description })) : null] }, agent.name))) })] })) : null;
|
|
329
338
|
// Share-link control. Sits to the left of the panel's built-in
|
|
330
339
|
// fullscreen / close buttons so users can mint a public link without
|
|
@@ -423,7 +432,7 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
423
432
|
});
|
|
424
433
|
return false;
|
|
425
434
|
}
|
|
426
|
-
}, publishDraftsLabel: locale.publishDrafts, nextStepsLabel: locale.nextSteps, planTitleLabel: locale.planTitle, planQuestionsLabel: locale.planQuestions, planAssumptionsLabel: locale.planAssumptions, planApproveHintLabel: locale.planApproveHint, planApproveLabel: locale.planApprove, planAdjustLabel: locale.planAdjust, planApproveMessage: locale.planApproveMessage, planApproveDefaultsMessage: locale.planApproveDefaultsMessage, planAnswerMessage: locale.planAnswer, publishedLabel: locale.published,
|
|
435
|
+
}, publishDraftsLabel: locale.publishDrafts, nextStepsLabel: locale.nextSteps, planTitleLabel: locale.planTitle, planQuestionsLabel: locale.planQuestions, planAssumptionsLabel: locale.planAssumptions, planApproveHintLabel: locale.planApproveHint, planApproveLabel: locale.planApprove, planAdjustLabel: locale.planAdjust, planBuiltLabel: locale.planBuilt, planApproveMessage: locale.planApproveMessage, planApproveDefaultsMessage: locale.planApproveDefaultsMessage, planAnswerMessage: locale.planAnswer, publishedLabel: locale.published,
|
|
427
436
|
// Self-use "magic moment": when the plan enables it, auto-publish the
|
|
428
437
|
// drafted app the instant the agent finishes — the success path above
|
|
429
438
|
// then navigates straight to the live app, so "build" lands the user on
|
|
@@ -434,10 +443,18 @@ export default function ConsoleFloatingChatbot({ appLabel, appName, objects, api
|
|
|
434
443
|
const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
|
|
435
444
|
const env = import.meta.env ?? {};
|
|
436
445
|
const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
|
|
437
|
-
// Power-user / admin escape hatch: force the picker on globally without
|
|
438
|
-
// touching app metadata.
|
|
439
|
-
const showAgentPicker = showAgentPickerProp ?? env.VITE_AI_SHOW_AGENT_PICKER === 'true';
|
|
440
446
|
const { agents, isLoading: agentsLoading, error: agentsError } = useAgents({ apiBase });
|
|
447
|
+
// Reveal the Build/Ask switcher only when AI development is unlocked for this
|
|
448
|
+
// viewer — the live catalog serves BOTH an `ask` and a `build` agent and
|
|
449
|
+
// authoring isn't deployment-disabled. Pure end-user apps (only `ask`) stay
|
|
450
|
+
// clean; builders can flip "ask about my data" ↔ "extend my app" inline. An
|
|
451
|
+
// explicit prop or `VITE_AI_SHOW_AGENT_PICKER` still forces it. See agentPicker.
|
|
452
|
+
const showAgentPicker = shouldShowAgentPicker({
|
|
453
|
+
agents,
|
|
454
|
+
showAgentPickerProp,
|
|
455
|
+
envOptIn: env.VITE_AI_SHOW_AGENT_PICKER === 'true',
|
|
456
|
+
aiStudioEnabled: getRuntimeConfig().features.aiStudio !== false,
|
|
457
|
+
});
|
|
441
458
|
const [activeAgent, setActiveAgent] = React.useState(undefined);
|
|
442
459
|
React.useEffect(() => {
|
|
443
460
|
if (!activeAgent && agents.length > 0) {
|
|
@@ -19,6 +19,16 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|
|
19
19
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
20
20
|
import { getIcon } from '../utils/getIcon';
|
|
21
21
|
import { resolveI18nLabel } from '../utils';
|
|
22
|
+
// Local/Custom scope sentinel — kept inline (not imported from metadata-admin)
|
|
23
|
+
// so this layout module never forms an import cycle with the metadata-admin
|
|
24
|
+
// views. Mirrors `LOCAL_PACKAGE_ID` in views/metadata-admin/package-scope.ts.
|
|
25
|
+
const LOCAL_SCOPE_ID = 'sys_metadata';
|
|
26
|
+
function localScopeLabel() {
|
|
27
|
+
const lang = (typeof document !== 'undefined' && document.documentElement?.lang) ||
|
|
28
|
+
(typeof navigator !== 'undefined' && navigator.language) ||
|
|
29
|
+
'';
|
|
30
|
+
return /^zh/i.test(lang) ? '本地 / 自定义(本环境)' : 'Local / Custom (this env)';
|
|
31
|
+
}
|
|
22
32
|
const ALL_SENTINEL = '__all__';
|
|
23
33
|
/** Read a (possibly dotted) property path off a row, e.g. `manifest.id`. */
|
|
24
34
|
function getByPath(row, key) {
|
|
@@ -80,43 +90,56 @@ function useSelectorOptions(def) {
|
|
|
80
90
|
const labelKey = def.optionsSource.labelKey || 'name';
|
|
81
91
|
const filters = def.optionsSource.filter;
|
|
82
92
|
const filterKey = JSON.stringify(filters ?? []);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
opts.sort((a, b) => a.label.localeCompare(b.label));
|
|
110
|
-
setOptions(opts);
|
|
93
|
+
// Stable, re-runnable fetch. Without an explicit refetch the option list is
|
|
94
|
+
// read once on mount, so a package created elsewhere (PackagesPage) stays
|
|
95
|
+
// invisible in this dropdown until a full page reload. We refetch on
|
|
96
|
+
// dropdown-open and on a global `objectui:packages-changed` signal.
|
|
97
|
+
const load = React.useCallback(async () => {
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(endpoint, {
|
|
100
|
+
credentials: 'include',
|
|
101
|
+
headers: { Accept: 'application/json' },
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok)
|
|
104
|
+
return;
|
|
105
|
+
const json = await res.json();
|
|
106
|
+
const rows = extractRows(json);
|
|
107
|
+
const opts = [];
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
if (!rowPasses(row, filters))
|
|
111
|
+
continue;
|
|
112
|
+
const value = getByPath(row, valueKey);
|
|
113
|
+
if (value == null || typeof value !== 'string' || seen.has(value))
|
|
114
|
+
continue;
|
|
115
|
+
seen.add(value);
|
|
116
|
+
const labelRaw = getByPath(row, labelKey);
|
|
117
|
+
opts.push({ value, label: typeof labelRaw === 'string' && labelRaw ? labelRaw : value });
|
|
111
118
|
}
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
opts.sort((a, b) => a.label.localeCompare(b.label));
|
|
120
|
+
// The package-scope selector gets a stable "Local / Custom (this env)"
|
|
121
|
+
// entry for this environment's runtime, DB-authored metadata — it is
|
|
122
|
+
// never a real package row (`package_id = null` / `sys_metadata`
|
|
123
|
+
// provenance) yet must always be selectable so org-authored items are
|
|
124
|
+
// discoverable and editable. The metadata list/get API already treats
|
|
125
|
+
// `?package=sys_metadata` as exactly this local scope.
|
|
126
|
+
if (/package/i.test(endpoint) && !opts.some((o) => o.value === LOCAL_SCOPE_ID)) {
|
|
127
|
+
opts.push({ value: LOCAL_SCOPE_ID, label: localScopeLabel() });
|
|
114
128
|
}
|
|
115
|
-
|
|
116
|
-
|
|
129
|
+
setOptions(opts);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* offline / unauthorized — render with no options */
|
|
133
|
+
}
|
|
117
134
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
118
135
|
}, [endpoint, valueKey, labelKey, filterKey]);
|
|
119
|
-
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
void load();
|
|
138
|
+
const onChanged = () => { void load(); };
|
|
139
|
+
window.addEventListener('objectui:packages-changed', onChanged);
|
|
140
|
+
return () => window.removeEventListener('objectui:packages-changed', onChanged);
|
|
141
|
+
}, [load]);
|
|
142
|
+
return { options, refetch: load };
|
|
120
143
|
}
|
|
121
144
|
/**
|
|
122
145
|
* Hook: resolves the active values for an app's context selectors and
|
|
@@ -189,7 +212,7 @@ export function useAppContextSelectors(appName, selectors, t) {
|
|
|
189
212
|
return { contextValues, element };
|
|
190
213
|
}
|
|
191
214
|
function SelectorControl({ def, value, onChange, t, }) {
|
|
192
|
-
const options = useSelectorOptions(def);
|
|
215
|
+
const { options, refetch } = useSelectorOptions(def);
|
|
193
216
|
const Icon = getIcon(def.icon);
|
|
194
217
|
const rawLabel = resolveI18nLabel(def.label, t) || def.id;
|
|
195
218
|
const label = rawLabel === 'Package'
|
|
@@ -214,5 +237,6 @@ function SelectorControl({ def, value, onChange, t, }) {
|
|
|
214
237
|
}
|
|
215
238
|
}, [hasConcrete, onChange, options]);
|
|
216
239
|
const current = hasConcrete ? value : '';
|
|
217
|
-
return (_jsxs(Select, { value: current, onValueChange: onChange,
|
|
240
|
+
return (_jsxs(Select, { value: current, onValueChange: onChange, onOpenChange: (open) => { if (open)
|
|
241
|
+
refetch(); }, children: [_jsx(SelectTrigger, { "aria-label": label, "data-testid": "package-switcher", className: "h-9 w-full gap-2 rounded-md border-sidebar-border/70 bg-sidebar/80 px-2 text-xs font-medium text-sidebar-foreground shadow-none transition-colors hover:bg-sidebar-accent focus:ring-1 focus:ring-sidebar-ring data-[state=open]:bg-sidebar-accent [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0", children: _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden", children: [_jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded border border-sidebar-border/70 bg-sidebar-accent text-sidebar-foreground/70", children: _jsx(Icon, { className: "h-3 w-3" }) }), _jsx(SelectValue, { placeholder: placeholder, className: "truncate" })] }) }), _jsx(SelectContent, { className: "w-[var(--radix-select-trigger-width)]", children: options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
218
242
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentPicker
|
|
3
|
+
*
|
|
4
|
+
* Pure decision logic for the floating assistant's Build/Ask switcher. Kept in
|
|
5
|
+
* its own module (no React, no chat deps) so it can be unit-tested without
|
|
6
|
+
* dragging in the heavy chat component graph (FloatingChatbot → streamdown →
|
|
7
|
+
* shiki → @ai-sdk, ~20MB) that `ConsoleFloatingChatbot` pulls in.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { type AgentDescriptor } from '@object-ui/plugin-chatbot';
|
|
11
|
+
/** Minimal catalog shape the decision needs — just the agent name. */
|
|
12
|
+
type AgentLike = Pick<AgentDescriptor, 'name'>;
|
|
13
|
+
export interface AgentPickerDecisionInput {
|
|
14
|
+
/** Live agent catalog from `useAgents` (the single source of truth). */
|
|
15
|
+
agents: AgentLike[];
|
|
16
|
+
/**
|
|
17
|
+
* Explicit host override. When defined it wins outright — `true` forces the
|
|
18
|
+
* switcher on, `false` forces it off — regardless of catalog or env.
|
|
19
|
+
*/
|
|
20
|
+
showAgentPickerProp?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* `VITE_AI_SHOW_AGENT_PICKER === 'true'` — the power-user / admin global
|
|
23
|
+
* escape hatch that forces the switcher on without touching app metadata.
|
|
24
|
+
*/
|
|
25
|
+
envOptIn?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether AI Studio (authoring / "online development") is enabled for this
|
|
28
|
+
* deployment. When off, the build agent must not be reachable from the panel,
|
|
29
|
+
* so the auto-reveal is suppressed even if the catalog still serves `build`.
|
|
30
|
+
* Mirrors `ConsoleLayout`'s `aiStudioEnabled` gate. Defaults to true.
|
|
31
|
+
*/
|
|
32
|
+
aiStudioEnabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True when the live catalog exposes BOTH a data/query (`ask`) and an authoring
|
|
36
|
+
* (`build`) agent — alias-aware via {@link isAskAgent}/{@link isBuildAgent}, so
|
|
37
|
+
* a catalog still serving the legacy `data_chat`/`metadata_assistant` ids counts
|
|
38
|
+
* too. This is the "AI development is unlocked for this viewer" signal, the same
|
|
39
|
+
* `askAvailable && buildAvailable` notion HomePage uses to surface "Build with AI".
|
|
40
|
+
*/
|
|
41
|
+
export declare function isAiDevUnlocked(agents: AgentLike[]): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether the floating assistant should reveal its Build/Ask switcher.
|
|
44
|
+
*
|
|
45
|
+
* Restrained by design (the original "end users shouldn't have to choose" rule):
|
|
46
|
+
* a pure end-user surface bound to a single `ask` agent never sees it. Precedence:
|
|
47
|
+
* 1. `showAgentPickerProp` — explicit host override wins (`true`/`false`).
|
|
48
|
+
* 2. `envOptIn` — `VITE_AI_SHOW_AGENT_PICKER` forces it on globally.
|
|
49
|
+
* 3. Auto-reveal — AI development is unlocked ({@link isAiDevUnlocked}) AND
|
|
50
|
+
* authoring isn't deployment-disabled (`aiStudioEnabled`).
|
|
51
|
+
*
|
|
52
|
+
* Returns the *intent* only: the render site still requires more than one agent
|
|
53
|
+
* (`agents.length > 1`) to draw an actual choice.
|
|
54
|
+
*/
|
|
55
|
+
export declare function shouldShowAgentPicker({ agents, showAgentPickerProp, envOptIn, aiStudioEnabled, }: AgentPickerDecisionInput): boolean;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentPicker
|
|
3
|
+
*
|
|
4
|
+
* Pure decision logic for the floating assistant's Build/Ask switcher. Kept in
|
|
5
|
+
* its own module (no React, no chat deps) so it can be unit-tested without
|
|
6
|
+
* dragging in the heavy chat component graph (FloatingChatbot → streamdown →
|
|
7
|
+
* shiki → @ai-sdk, ~20MB) that `ConsoleFloatingChatbot` pulls in.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { isAskAgent, isBuildAgent } from '@object-ui/plugin-chatbot';
|
|
11
|
+
/**
|
|
12
|
+
* True when the live catalog exposes BOTH a data/query (`ask`) and an authoring
|
|
13
|
+
* (`build`) agent — alias-aware via {@link isAskAgent}/{@link isBuildAgent}, so
|
|
14
|
+
* a catalog still serving the legacy `data_chat`/`metadata_assistant` ids counts
|
|
15
|
+
* too. This is the "AI development is unlocked for this viewer" signal, the same
|
|
16
|
+
* `askAvailable && buildAvailable` notion HomePage uses to surface "Build with AI".
|
|
17
|
+
*/
|
|
18
|
+
export function isAiDevUnlocked(agents) {
|
|
19
|
+
return (agents.some((a) => isAskAgent(a.name)) && agents.some((a) => isBuildAgent(a.name)));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Decide whether the floating assistant should reveal its Build/Ask switcher.
|
|
23
|
+
*
|
|
24
|
+
* Restrained by design (the original "end users shouldn't have to choose" rule):
|
|
25
|
+
* a pure end-user surface bound to a single `ask` agent never sees it. Precedence:
|
|
26
|
+
* 1. `showAgentPickerProp` — explicit host override wins (`true`/`false`).
|
|
27
|
+
* 2. `envOptIn` — `VITE_AI_SHOW_AGENT_PICKER` forces it on globally.
|
|
28
|
+
* 3. Auto-reveal — AI development is unlocked ({@link isAiDevUnlocked}) AND
|
|
29
|
+
* authoring isn't deployment-disabled (`aiStudioEnabled`).
|
|
30
|
+
*
|
|
31
|
+
* Returns the *intent* only: the render site still requires more than one agent
|
|
32
|
+
* (`agents.length > 1`) to draw an actual choice.
|
|
33
|
+
*/
|
|
34
|
+
export function shouldShowAgentPicker({ agents, showAgentPickerProp, envOptIn = false, aiStudioEnabled = true, }) {
|
|
35
|
+
if (showAgentPickerProp !== undefined)
|
|
36
|
+
return showAgentPickerProp;
|
|
37
|
+
if (envOptIn)
|
|
38
|
+
return true;
|
|
39
|
+
return aiStudioEnabled && isAiDevUnlocked(agents);
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
export interface CommitTimelineProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
packageId: string;
|
|
12
|
+
/** Called after a successful revert so the host can refresh the app view. */
|
|
13
|
+
onReverted?: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function CommitTimeline({ open, onOpenChange, packageId, onReverted }: CommitTimelineProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* ADR-0067 — the package "Build history" timeline. Lists every commit an AI
|
|
11
|
+
* build (or edit) landed, newest-first, with "Revert" per apply commit: undo
|
|
12
|
+
* that change set (artifacts it created are soft-removed; ones it edited are
|
|
13
|
+
* restored) as a NEW append-only revert commit. This is the history-not-confirm
|
|
14
|
+
* surface — the user reviews and reverts instead of approving each publish.
|
|
15
|
+
*
|
|
16
|
+
* Sibling of DraftChangesPanel (which lists PENDING drafts before a publish);
|
|
17
|
+
* this lists what already LANDED, and can undo it.
|
|
18
|
+
*/
|
|
19
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
20
|
+
import { GitBranch, Undo2, Loader2 } from 'lucide-react';
|
|
21
|
+
import { toast } from 'sonner';
|
|
22
|
+
import { Badge, Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@object-ui/components';
|
|
23
|
+
import { useObjectTranslation } from '@object-ui/i18n';
|
|
24
|
+
import { fetchCommits, revertCommit } from './commitHistory';
|
|
25
|
+
function relativeTime(iso) {
|
|
26
|
+
if (!iso)
|
|
27
|
+
return '';
|
|
28
|
+
const then = Date.parse(iso);
|
|
29
|
+
if (Number.isNaN(then))
|
|
30
|
+
return '';
|
|
31
|
+
const secs = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
32
|
+
if (secs < 60)
|
|
33
|
+
return `${secs}s`;
|
|
34
|
+
const mins = Math.round(secs / 60);
|
|
35
|
+
if (mins < 60)
|
|
36
|
+
return `${mins}m`;
|
|
37
|
+
const hrs = Math.round(mins / 60);
|
|
38
|
+
if (hrs < 24)
|
|
39
|
+
return `${hrs}h`;
|
|
40
|
+
return `${Math.round(hrs / 24)}d`;
|
|
41
|
+
}
|
|
42
|
+
export function CommitTimeline({ open, onOpenChange, packageId, onReverted }) {
|
|
43
|
+
const { t } = useObjectTranslation();
|
|
44
|
+
const [commits, setCommits] = useState(null);
|
|
45
|
+
const [error, setError] = useState(null);
|
|
46
|
+
const [reverting, setReverting] = useState(null);
|
|
47
|
+
const load = useCallback(async () => {
|
|
48
|
+
setCommits(null);
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
setCommits(await fetchCommits(packageId));
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
setError(e.message);
|
|
55
|
+
}
|
|
56
|
+
}, [packageId]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (open)
|
|
59
|
+
void load();
|
|
60
|
+
}, [open, load]);
|
|
61
|
+
const onRevert = async (commitId) => {
|
|
62
|
+
setReverting(commitId);
|
|
63
|
+
try {
|
|
64
|
+
await revertCommit(packageId, commitId);
|
|
65
|
+
toast.success(t('preview.history.reverted', { defaultValue: 'Reverted — the change has been undone.' }));
|
|
66
|
+
onReverted?.();
|
|
67
|
+
await load();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
toast.error(`${t('preview.history.revertFailed', { defaultValue: 'Revert failed' })}: ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
setReverting(null);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", className: "w-[420px] sm:max-w-[420px]", "data-testid": "commit-timeline-panel", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: t('preview.history.title', { defaultValue: 'Build history' }) }), _jsx(SheetDescription, { children: t('preview.history.description', {
|
|
77
|
+
defaultValue: 'Every change to this app, newest first. Revert any step to undo it — no publish confirmation needed.',
|
|
78
|
+
}) })] }), _jsx("div", { className: "mt-4 flex flex-col gap-2 overflow-y-auto px-4 pb-6", children: error ? (_jsxs("p", { className: "text-sm text-destructive", children: [t('preview.history.loadFailed', { defaultValue: 'Could not load history:' }), " ", error] })) : commits === null ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), t('preview.history.loading', { defaultValue: 'Loading history…' })] })) : commits.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('preview.history.empty', { defaultValue: 'No history yet for this app.' }) })) : (commits.map((c) => (_jsxs("div", { className: "flex items-start gap-2.5 rounded-md border px-2.5 py-2 text-sm", "data-testid": "commit-row", children: [_jsx(GitBranch, { className: `mt-0.5 h-4 w-4 shrink-0 ${c.operation === 'revert' ? 'text-muted-foreground' : 'text-primary'}` }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate font-medium", children: c.message ??
|
|
79
|
+
(c.operation === 'revert'
|
|
80
|
+
? t('preview.history.revertLabel', { defaultValue: 'Reverted a change' })
|
|
81
|
+
: t('preview.history.applyLabel', { defaultValue: 'Build change' })) }), _jsxs("p", { className: "mt-0.5 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground", children: [c.operation === 'revert' ? (_jsx(Badge, { variant: "outline", className: "border-muted-foreground/30", children: t('preview.history.revert', { defaultValue: 'revert' }) })) : null, _jsxs("span", { children: [c.itemCount, " ", t('preview.history.items', { defaultValue: 'item(s)' })] }), c.actor ? _jsxs("span", { children: ["\u00B7 ", c.actor] }) : null, c.createdAt ? _jsxs("span", { children: ["\u00B7 ", relativeTime(c.createdAt)] }) : null] })] }), c.operation === 'apply' ? (_jsx(Button, { size: "sm", variant: "ghost", className: "shrink-0", disabled: reverting !== null, onClick: () => onRevert(c.id), "data-testid": "commit-revert", children: reverting === c.id ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(Undo2, { className: "mr-1 h-3.5 w-3.5" }), t('preview.history.revertAction', { defaultValue: 'Revert' })] })) })) : null] }, c.id)))) })] }) }));
|
|
82
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
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
2
|
/**
|
|
3
3
|
* ObjectUI
|
|
4
4
|
* Copyright (c) 2024-present ObjectStack Inc.
|
|
@@ -20,12 +20,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
20
20
|
*/
|
|
21
21
|
import { useState } from 'react';
|
|
22
22
|
import { useLocation, useParams } from 'react-router-dom';
|
|
23
|
-
import { EyeOff, Rocket } from 'lucide-react';
|
|
23
|
+
import { EyeOff, History, Rocket } from 'lucide-react';
|
|
24
24
|
import { toast } from 'sonner';
|
|
25
25
|
import { Button } from '@object-ui/components';
|
|
26
26
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
27
27
|
import { useMetadata } from '../providers/MetadataProvider';
|
|
28
28
|
import { matchAppBySegment } from '../utils/appRoute';
|
|
29
|
+
import { CommitTimeline } from './CommitTimeline';
|
|
29
30
|
import { usePreviewDrafts } from './PreviewModeContext';
|
|
30
31
|
export function UnpublishedAppBar() {
|
|
31
32
|
const preview = usePreviewDrafts();
|
|
@@ -34,6 +35,7 @@ export function UnpublishedAppBar() {
|
|
|
34
35
|
const { apps, refresh } = useMetadata();
|
|
35
36
|
const { t } = useObjectTranslation();
|
|
36
37
|
const [publishing, setPublishing] = useState(false);
|
|
38
|
+
const [historyOpen, setHistoryOpen] = useState(false);
|
|
37
39
|
// The draft-preview watermark owns the preview tree; never stack both bars.
|
|
38
40
|
if (preview)
|
|
39
41
|
return null;
|
|
@@ -43,6 +45,8 @@ export function UnpublishedAppBar() {
|
|
|
43
45
|
const app = matchAppBySegment(apps ?? [], routeApp);
|
|
44
46
|
if (!app || app.hidden !== true)
|
|
45
47
|
return null;
|
|
48
|
+
// ADR-0067 — the package this app belongs to keys its commit timeline.
|
|
49
|
+
const packageId = app?.packageId ?? app?._packageId ?? null;
|
|
46
50
|
const publish = async () => {
|
|
47
51
|
setPublishing(true);
|
|
48
52
|
try {
|
|
@@ -71,9 +75,9 @@ export function UnpublishedAppBar() {
|
|
|
71
75
|
setPublishing(false);
|
|
72
76
|
}
|
|
73
77
|
};
|
|
74
|
-
return (_jsxs("div", { className: "sticky top-0 z-40 flex items-center gap-3 border-b border-amber-300/70 bg-amber-50 px-4 py-2 text-sm text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200", "data-testid": "unpublished-app-bar", children: [_jsx(EyeOff, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: t('preview.unpublishedBar.message', {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sticky top-0 z-40 flex items-center gap-3 border-b border-amber-300/70 bg-amber-50 px-4 py-2 text-sm text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200", "data-testid": "unpublished-app-bar", children: [_jsx(EyeOff, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: t('preview.unpublishedBar.message', {
|
|
79
|
+
defaultValue: 'Unpublished app — fully functional, but only builders can see it. Publish to make it visible to your users.',
|
|
80
|
+
}) }), packageId ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setHistoryOpen(true), "data-testid": "unpublished-app-history", children: [_jsx(History, { className: "mr-1 h-3.5 w-3.5" }), t('preview.history.button', { defaultValue: 'History' })] })) : null, _jsxs(Button, { size: "sm", onClick: publish, disabled: publishing, "data-testid": "unpublished-app-publish", children: [_jsx(Rocket, { className: "mr-1 h-3.5 w-3.5" }), publishing
|
|
81
|
+
? t('preview.unpublishedBar.publishing', { defaultValue: 'Publishing…' })
|
|
82
|
+
: t('preview.unpublishedBar.publish', { defaultValue: 'Publish' })] })] }), packageId ? (_jsx(CommitTimeline, { open: historyOpen, onOpenChange: setHistoryOpen, packageId: packageId, onReverted: refresh })) : null] }));
|
|
79
83
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* ADR-0067 — package-scoped commit history reads/writes for the timeline.
|
|
10
|
+
*
|
|
11
|
+
* Each AI build (and Studio batch) lands as one revertible COMMIT on top of
|
|
12
|
+
* the metadata history. The history-not-confirm model: you don't approve each
|
|
13
|
+
* publish — you review this timeline and revert if a change was wrong. These
|
|
14
|
+
* helpers read the timeline (`GET /packages/:id/commits`) and revert one
|
|
15
|
+
* commit (`POST /packages/:id/commits/:cid/revert`). Cookie-authenticated like
|
|
16
|
+
* every console call; tolerant of the `{ data: ... }` / bare envelope shapes.
|
|
17
|
+
*/
|
|
18
|
+
export interface CommitEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
operation: 'apply' | 'revert';
|
|
21
|
+
message?: string;
|
|
22
|
+
actor?: string;
|
|
23
|
+
aiModel?: string;
|
|
24
|
+
itemCount: number;
|
|
25
|
+
createdAt?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function fetchCommits(packageId: string): Promise<CommitEntry[]>;
|
|
28
|
+
export declare function revertCommit(packageId: string, commitId: string): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchCommits(packageId) {
|
|
9
|
+
const res = await fetch(`/api/v1/packages/${encodeURIComponent(packageId)}/commits`, {
|
|
10
|
+
credentials: 'include',
|
|
11
|
+
headers: { Accept: 'application/json' },
|
|
12
|
+
cache: 'no-store',
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
throw new Error(`commits HTTP ${res.status}`);
|
|
16
|
+
const data = (await res.json());
|
|
17
|
+
const list = Array.isArray(data)
|
|
18
|
+
? data
|
|
19
|
+
: data?.commits ??
|
|
20
|
+
data?.data?.commits ??
|
|
21
|
+
[];
|
|
22
|
+
return (Array.isArray(list) ? list : []).map((raw) => {
|
|
23
|
+
const c = raw;
|
|
24
|
+
return {
|
|
25
|
+
id: String(c.id),
|
|
26
|
+
operation: c.operation === 'revert' ? 'revert' : 'apply',
|
|
27
|
+
message: typeof c.message === 'string' ? c.message : undefined,
|
|
28
|
+
actor: typeof c.actor === 'string' ? c.actor : undefined,
|
|
29
|
+
aiModel: typeof c.aiModel === 'string' ? c.aiModel : undefined,
|
|
30
|
+
itemCount: typeof c.itemCount === 'number' ? c.itemCount : 0,
|
|
31
|
+
createdAt: typeof c.createdAt === 'string' ? c.createdAt : undefined,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function revertCommit(packageId, commitId) {
|
|
36
|
+
const res = await fetch(`/api/v1/packages/${encodeURIComponent(packageId)}/commits/${encodeURIComponent(commitId)}/revert`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
credentials: 'include',
|
|
39
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
40
|
+
body: '{}',
|
|
41
|
+
});
|
|
42
|
+
const payload = (await res.json().catch(() => null));
|
|
43
|
+
const inner = payload?.data ?? payload ?? undefined;
|
|
44
|
+
if (!res.ok || inner?.success === false) {
|
|
45
|
+
const err = payload?.error;
|
|
46
|
+
throw new Error((typeof err === 'object' ? err?.message : err) ?? `HTTP ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -459,6 +459,15 @@ export function MetadataProvider({ children, adapter, ttlMs = DEFAULT_TTL_MS })
|
|
|
459
459
|
// NOT gated to preview-drafts mode: the live world every tree reads changed.
|
|
460
460
|
useEffect(() => {
|
|
461
461
|
return subscribeMetadataRefresh(() => {
|
|
462
|
+
// The adapter keeps its OWN object-schema cache (getObjectSchema) that the
|
|
463
|
+
// context refresh below does not touch. A publish/install just changed the
|
|
464
|
+
// live schema, so drop it too — otherwise create/edit forms (which read
|
|
465
|
+
// the adapter's getObjectSchema, NOT this context) keep showing the
|
|
466
|
+
// pre-publish field set until the adapter cache's 5-minute TTL lapses.
|
|
467
|
+
try {
|
|
468
|
+
adapterRef.current?.clearCache?.();
|
|
469
|
+
}
|
|
470
|
+
catch { /* adapter mid-swap */ }
|
|
462
471
|
void refresh();
|
|
463
472
|
});
|
|
464
473
|
}, [refresh]);
|
|
@@ -1,33 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
label?: string;
|
|
4
|
-
type?: string;
|
|
5
|
-
required?: boolean;
|
|
6
|
-
options?: Array<{
|
|
7
|
-
value: unknown;
|
|
8
|
-
label: string;
|
|
9
|
-
}>;
|
|
10
|
-
defaultValue?: unknown;
|
|
11
|
-
placeholder?: string;
|
|
12
|
-
}
|
|
13
|
-
export interface ScreenSpec {
|
|
14
|
-
nodeId: string;
|
|
15
|
-
title?: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
fields: ScreenFieldSpec[];
|
|
18
|
-
/**
|
|
19
|
-
* `'object-form'` renders the named object's FULL create/edit form — incl.
|
|
20
|
-
* inline master-detail child grids — as a wizard step (vs. the flat `fields`
|
|
21
|
-
* list). The form persists the record (and its children, atomically) itself,
|
|
22
|
-
* then resumes the run with the saved id bound to `idVariable`.
|
|
23
|
-
*/
|
|
24
|
-
kind?: 'fields' | 'object-form';
|
|
25
|
-
objectName?: string;
|
|
26
|
-
mode?: 'create' | 'edit';
|
|
27
|
-
recordId?: string;
|
|
28
|
-
defaults?: Record<string, unknown>;
|
|
29
|
-
idVariable?: string;
|
|
30
|
-
}
|
|
1
|
+
import { type ScreenSpec } from './ScreenView';
|
|
2
|
+
export type { ScreenSpec, ScreenFieldSpec } from './ScreenView';
|
|
31
3
|
export interface ScreenFlowState {
|
|
32
4
|
flowName: string;
|
|
33
5
|
runId: string;
|