@object-ui/app-shell 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +279 -0
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +36 -9
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.js +16 -2
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +21 -4
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +15 -0
- package/dist/views/metadata-admin/package-scope.js +16 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -17,7 +17,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
17
17
|
*/
|
|
18
18
|
import * as React from 'react';
|
|
19
19
|
import { Link, useLocation } from 'react-router-dom';
|
|
20
|
-
import { Package as PackageIcon, Plus, RefreshCw, Search, Upload, Download, FileUp, Undo2, Power, PowerOff, ExternalLink, AlertTriangle, Trash2, } from 'lucide-react';
|
|
20
|
+
import { Package as PackageIcon, Plus, RefreshCw, Search, Upload, Download, FileUp, Undo2, Power, PowerOff, ExternalLink, AlertTriangle, Trash2, Copy, Inbox, } from 'lucide-react';
|
|
21
21
|
import { Button, Input, Badge, Switch, Label, Separator, Skeleton, Empty, EmptyTitle, EmptyDescription, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@object-ui/components';
|
|
22
22
|
import { detectLocale, t, tFormat } from './i18n';
|
|
23
23
|
const API = '/api/v1/packages';
|
|
@@ -65,7 +65,7 @@ function StatusBadge({ pkg }) {
|
|
|
65
65
|
/* -------------------------------------------------------------------------- */
|
|
66
66
|
const ID_RE = /^[a-z0-9][a-z0-9._-]{1,254}$/i;
|
|
67
67
|
const VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
68
|
-
function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
68
|
+
export function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
69
69
|
const locale = React.useMemo(() => detectLocale(), []);
|
|
70
70
|
const [id, setId] = React.useState('');
|
|
71
71
|
const [name, setName] = React.useState('');
|
|
@@ -242,10 +242,13 @@ function PackageDetailSheet({ pkg, appBase, open, onOpenChange, onChanged, }) {
|
|
|
242
242
|
const ok = window.confirm(tFormat('engine.packages.detail.deleteConfirm', locale, { name: pkg?.manifest.name || id }));
|
|
243
243
|
if (!ok)
|
|
244
244
|
return;
|
|
245
|
+
// ADR-0070 D4 (Q3) — let the user keep records (delete structure only).
|
|
246
|
+
const alsoData = window.confirm(t('engine.packages.detail.deleteKeepData', locale));
|
|
247
|
+
const qs = alsoData ? '' : '?keepData=true';
|
|
245
248
|
setBusy('delete');
|
|
246
249
|
setMsg(null);
|
|
247
250
|
try {
|
|
248
|
-
await apiJson(`${API}/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
251
|
+
await apiJson(`${API}/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' });
|
|
249
252
|
onChanged();
|
|
250
253
|
onOpenChange(false);
|
|
251
254
|
}
|
|
@@ -256,6 +259,48 @@ function PackageDetailSheet({ pkg, appBase, open, onOpenChange, onChanged, }) {
|
|
|
256
259
|
setBusy(null);
|
|
257
260
|
}
|
|
258
261
|
};
|
|
262
|
+
// ADR-0070 D4 — duplicate this base into a NEW writable package (re-namespaced).
|
|
263
|
+
const duplicateApp = async () => {
|
|
264
|
+
const target = window.prompt(t('engine.packages.detail.duplicatePrompt', locale), `${id}-copy`);
|
|
265
|
+
if (!target || !target.trim())
|
|
266
|
+
return;
|
|
267
|
+
setBusy('duplicate');
|
|
268
|
+
setMsg(null);
|
|
269
|
+
try {
|
|
270
|
+
await apiJson(`${API}/${encodeURIComponent(id)}/duplicate`, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: { 'Content-Type': 'application/json' },
|
|
273
|
+
body: JSON.stringify({ targetPackageId: target.trim(), targetName: `${pkg?.manifest.name ?? id} (copy)` }),
|
|
274
|
+
});
|
|
275
|
+
setMsg({ kind: 'ok', text: t('engine.packages.detail.duplicated', locale) });
|
|
276
|
+
onChanged();
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
setMsg({ kind: 'err', text: e?.message ?? 'Duplicate failed' });
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
setBusy(null);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
// ADR-0070 D5 — adopt every package-less (loose) item in this env INTO this base.
|
|
286
|
+
const adoptOrphans = async () => {
|
|
287
|
+
const ok = window.confirm(tFormat('engine.packages.detail.adoptConfirm', locale, { name: pkg?.manifest.name || id }));
|
|
288
|
+
if (!ok)
|
|
289
|
+
return;
|
|
290
|
+
setBusy('adopt');
|
|
291
|
+
setMsg(null);
|
|
292
|
+
try {
|
|
293
|
+
await apiJson(`${API}/${encodeURIComponent(id)}/adopt-orphans`, { method: 'POST' });
|
|
294
|
+
setMsg({ kind: 'ok', text: t('engine.packages.detail.adopted', locale) });
|
|
295
|
+
onChanged();
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
setMsg({ kind: 'err', text: e?.message ?? 'Adopt failed' });
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
setBusy(null);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
259
304
|
const toggleEnable = () => run('toggle', () => apiJson(`${API}/${encodeURIComponent(id)}/${enabled ? 'disable' : 'enable'}`, {
|
|
260
305
|
method: 'PATCH',
|
|
261
306
|
}), enabled ? t('engine.packages.detail.disabled', locale) : t('engine.packages.detail.enabled', locale));
|
|
@@ -277,7 +322,7 @@ function PackageDetailSheet({ pkg, appBase, open, onOpenChange, onChanged, }) {
|
|
|
277
322
|
? t('engine.packages.detail.publishing', locale)
|
|
278
323
|
: tFormat('engine.packages.detail.publishApp', locale, { count: drafts.length })] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: discardDrafts, disabled: !!busy, children: [_jsx(Undo2, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'discard-drafts'
|
|
279
324
|
? t('engine.packages.detail.discarding', locale)
|
|
280
|
-
: tFormat('engine.packages.detail.discardChanges', locale, { count: drafts.length })] })] })), _jsx("ul", { className: "space-y-1", children: drafts.map((d) => (_jsx("li", { children: _jsxs(Link, { to: `${appBase}/metadata/${encodeURIComponent(d.type)}/${encodeURIComponent(d.name)}?review=1`, className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline", onClick: () => onOpenChange(false), children: [_jsx(FileUp, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "font-mono text-xs", children: d.type }), _jsx("span", { className: "text-muted-foreground", children: "\u00B7" }), d.name] }) }, `${d.type}/${d.name}`))) })] })] })), _jsx(Separator, { className: "my-4" }), isKernel ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('engine.packages.detail.kernelReadOnly', locale) })) : (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.packages.detail.actions', locale) }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { size: "sm", onClick: publish, disabled: !!busy, children: [_jsx(Upload, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'publish' ? t('engine.packages.detail.publishing', locale) : t('engine.packages.detail.publish', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: revert, disabled: !!busy, children: [_jsx(Undo2, { className: "mr-1.5 h-3.5 w-3.5" }), t('engine.packages.detail.revert', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: toggleEnable, disabled: !!busy, children: [enabled ? (_jsx(PowerOff, { className: "mr-1.5 h-3.5 w-3.5" })) : (_jsx(Power, { className: "mr-1.5 h-3.5 w-3.5" })), enabled ? t('engine.packages.detail.disable', locale) : t('engine.packages.detail.enable', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: exportPkg, disabled: !!busy, children: [_jsx(Download, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'export' ? t('engine.packages.detail.exporting', locale) : t('engine.packages.detail.export', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: deleteApp, disabled: !!busy, className: "text-destructive hover:bg-destructive/10 hover:text-destructive", children: [_jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'delete' ? t('engine.packages.detail.deleting', locale) : t('engine.packages.detail.deleteApp', locale)] })] })] })), msg && (_jsx("div", { className: `mt-4 rounded-md border p-2 text-sm ${msg.kind === 'ok'
|
|
325
|
+
: tFormat('engine.packages.detail.discardChanges', locale, { count: drafts.length })] })] })), _jsx("ul", { className: "space-y-1", children: drafts.map((d) => (_jsx("li", { children: _jsxs(Link, { to: `${appBase}/metadata/${encodeURIComponent(d.type)}/${encodeURIComponent(d.name)}?review=1`, className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline", onClick: () => onOpenChange(false), children: [_jsx(FileUp, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "font-mono text-xs", children: d.type }), _jsx("span", { className: "text-muted-foreground", children: "\u00B7" }), d.name] }) }, `${d.type}/${d.name}`))) })] })] })), _jsx(Separator, { className: "my-4" }), isKernel ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('engine.packages.detail.kernelReadOnly', locale) })) : (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t('engine.packages.detail.actions', locale) }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { size: "sm", onClick: publish, disabled: !!busy, children: [_jsx(Upload, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'publish' ? t('engine.packages.detail.publishing', locale) : t('engine.packages.detail.publish', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: revert, disabled: !!busy, children: [_jsx(Undo2, { className: "mr-1.5 h-3.5 w-3.5" }), t('engine.packages.detail.revert', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: toggleEnable, disabled: !!busy, children: [enabled ? (_jsx(PowerOff, { className: "mr-1.5 h-3.5 w-3.5" })) : (_jsx(Power, { className: "mr-1.5 h-3.5 w-3.5" })), enabled ? t('engine.packages.detail.disable', locale) : t('engine.packages.detail.enable', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: exportPkg, disabled: !!busy, children: [_jsx(Download, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'export' ? t('engine.packages.detail.exporting', locale) : t('engine.packages.detail.export', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: duplicateApp, disabled: !!busy, children: [_jsx(Copy, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'duplicate' ? t('engine.packages.detail.duplicating', locale) : t('engine.packages.detail.duplicate', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: adoptOrphans, disabled: !!busy, children: [_jsx(Inbox, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'adopt' ? t('engine.packages.detail.adopting', locale) : t('engine.packages.detail.adoptOrphans', locale)] }), _jsxs(Button, { size: "sm", variant: "outline", onClick: deleteApp, disabled: !!busy, className: "text-destructive hover:bg-destructive/10 hover:text-destructive", children: [_jsx(Trash2, { className: "mr-1.5 h-3.5 w-3.5" }), busy === 'delete' ? t('engine.packages.detail.deleting', locale) : t('engine.packages.detail.deleteApp', locale)] })] })] })), msg && (_jsx("div", { className: `mt-4 rounded-md border p-2 text-sm ${msg.kind === 'ok'
|
|
281
326
|
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400'
|
|
282
327
|
: 'border-destructive/40 bg-destructive/10 text-destructive'}`, children: msg.text }))] }) }));
|
|
283
328
|
}
|
|
@@ -47,6 +47,7 @@ import { PageShell } from './PageShell';
|
|
|
47
47
|
import { useMetadataClient, useMetadataTypes } from './useMetadata';
|
|
48
48
|
import { resolveResourceConfig } from './registry';
|
|
49
49
|
import { t as translate, detectLocale } from './i18n';
|
|
50
|
+
import { AssignedUsersSection } from './AssignedUsersSection';
|
|
50
51
|
function getObjectActions(locale) {
|
|
51
52
|
return [
|
|
52
53
|
{ key: 'allowCreate', short: 'C', tip: translate('perm.action.create', locale) },
|
|
@@ -264,7 +265,7 @@ export function PermissionMatrixEditPage({ type, name }) {
|
|
|
264
265
|
if (loading) {
|
|
265
266
|
return (_jsx(PageShell, { entry: entry, itemName: name, children: _jsxs("div", { className: "p-6 text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " ", t('perm.loading').replace('{name}', name)] }) }));
|
|
266
267
|
}
|
|
267
|
-
return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: draft.isProfile ? t('perm.subtitle.profile') : t('perm.subtitle.set'), stats: stats, actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history?type=${encodeURIComponent(type)}`), children: [_jsx(HistoryIcon, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.history')] }), writable && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), t('engine.edit.save')] }))] }), children: [_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [error && (_jsxs("div", { className: "m-4 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive flex items-start gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: error })] })), _jsxs("div", { className: "px-6 py-3 border-b bg-muted/30 flex flex-wrap items-end gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "perm-name", className: "text-xs", children: t('perm.field.name') }), _jsx(Input, { id: "perm-name", value: draft.name, disabled: !writable, onChange: (e) => setDraft((p) => ({ ...p, name: e.target.value })), className: "h-8 w-56" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "perm-label", className: "text-xs", children: t('perm.field.label') }), _jsx(Input, { id: "perm-label", value: draft.label ?? '', disabled: !writable, onChange: (e) => setDraft((p) => ({ ...p, label: e.target.value })), className: "h-8 w-72" })] }), _jsxs("div", { className: "flex items-center gap-2 pb-1", children: [_jsx(Switch, { id: "perm-is-profile", checked: !!draft.isProfile, disabled: !writable, onCheckedChange: (v) => setDraft((p) => ({ ...p, isProfile: !!v })) }), _jsx(Label, { htmlFor: "perm-is-profile", className: "text-xs", children: t('perm.field.isProfile') })] }), !writable && (_jsx(Badge, { variant: "secondary", className: "ml-auto", children: t('perm.readOnly') }))] }), _jsxs("div", { className: "px-6 py-3 border-b flex items-center gap-3", children: [_jsx(Input, { placeholder: t('perm.filter.placeholder'), value: filter, onChange: (e) => setFilter(e.target.value), className: "h-8 w-72" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "only-enabled", checked: showOnlyEnabled, onCheckedChange: (v) => setShowOnlyEnabled(!!v) }), _jsx(Label, { htmlFor: "only-enabled", className: "text-xs", children: t('perm.filter.onlyGranted') })] }), _jsxs("span", { className: "text-xs text-muted-foreground ml-auto", children: [filteredObjects.length, " / ", objects.length, " ", t('perm.stat.objectsSuffix')] })] }), _jsx("div", { className: "flex-1 overflow-auto", children: _jsx(PermissionTable, { objects: filteredObjects, draft: draft, expanded: expanded, fieldsByObject: fieldsByObject, writable: writable, objectActions: OBJECT_ACTIONS, t: t, onToggleExpand: toggleExpand, onObjectPerm: updateObjectPerm, onFieldPerm: updateFieldPerm, onBulkSet: bulkSetObject }) })] }), _jsx(Dialog, { open: !!destructive, onOpenChange: (open) => !open && setDestructive(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), " ", t('engine.edit.destructive')] }), _jsx(DialogDescription, { children: t('engine.edit.destructiveHint') })] }), _jsx("ul", { className: "text-sm space-y-1 max-h-64 overflow-auto", children: destructive?.issues.map((i, idx) => (_jsxs("li", { className: "border-l-2 border-amber-500 pl-2", children: [i.kind && _jsx(Badge, { variant: "outline", className: "mr-2", children: i.kind }), i.message ?? JSON.stringify(i)] }, idx))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setDestructive(null), children: t('engine.cancel') }), _jsxs(Button, { variant: "destructive", onClick: () => destructive && doSave(true, destructive.pending), disabled: saving, children: [saving && _jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), t('engine.edit.forceSave')] })] })] }) })] }));
|
|
268
|
+
return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: draft.isProfile ? t('perm.subtitle.profile') : t('perm.subtitle.set'), stats: stats, actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history?type=${encodeURIComponent(type)}`), children: [_jsx(HistoryIcon, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.history')] }), writable && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving, children: [saving ? (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })) : (_jsx(Save, { className: "h-4 w-4 mr-1" })), t('engine.edit.save')] }))] }), children: [_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [error && (_jsxs("div", { className: "m-4 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive flex items-start gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 mt-0.5 shrink-0" }), _jsx("span", { children: error })] })), _jsxs("div", { className: "px-6 py-3 border-b bg-muted/30 flex flex-wrap items-end gap-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "perm-name", className: "text-xs", children: t('perm.field.name') }), _jsx(Input, { id: "perm-name", value: draft.name, disabled: !writable, onChange: (e) => setDraft((p) => ({ ...p, name: e.target.value })), className: "h-8 w-56" })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "perm-label", className: "text-xs", children: t('perm.field.label') }), _jsx(Input, { id: "perm-label", value: draft.label ?? '', disabled: !writable, onChange: (e) => setDraft((p) => ({ ...p, label: e.target.value })), className: "h-8 w-72" })] }), _jsxs("div", { className: "flex items-center gap-2 pb-1", children: [_jsx(Switch, { id: "perm-is-profile", checked: !!draft.isProfile, disabled: !writable, onCheckedChange: (v) => setDraft((p) => ({ ...p, isProfile: !!v })) }), _jsx(Label, { htmlFor: "perm-is-profile", className: "text-xs", children: t('perm.field.isProfile') })] }), !writable && (_jsx(Badge, { variant: "secondary", className: "ml-auto", children: t('perm.readOnly') }))] }), _jsxs("div", { className: "px-6 py-3 border-b flex items-center gap-3", children: [_jsx(Input, { placeholder: t('perm.filter.placeholder'), value: filter, onChange: (e) => setFilter(e.target.value), className: "h-8 w-72" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { id: "only-enabled", checked: showOnlyEnabled, onCheckedChange: (v) => setShowOnlyEnabled(!!v) }), _jsx(Label, { htmlFor: "only-enabled", className: "text-xs", children: t('perm.filter.onlyGranted') })] }), _jsxs("span", { className: "text-xs text-muted-foreground ml-auto", children: [filteredObjects.length, " / ", objects.length, " ", t('perm.stat.objectsSuffix')] })] }), _jsx("div", { className: "flex-1 overflow-auto", children: _jsx(PermissionTable, { objects: filteredObjects, draft: draft, expanded: expanded, fieldsByObject: fieldsByObject, writable: writable, objectActions: OBJECT_ACTIONS, t: t, onToggleExpand: toggleExpand, onObjectPerm: updateObjectPerm, onFieldPerm: updateFieldPerm, onBulkSet: bulkSetObject }) }), _jsx("div", { className: "shrink-0 border-t max-h-80 overflow-auto bg-background", children: _jsx(AssignedUsersSection, { permissionSetName: name }) })] }), _jsx(Dialog, { open: !!destructive, onOpenChange: (open) => !open && setDestructive(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-4 w-4 text-amber-500" }), " ", t('engine.edit.destructive')] }), _jsx(DialogDescription, { children: t('engine.edit.destructiveHint') })] }), _jsx("ul", { className: "text-sm space-y-1 max-h-64 overflow-auto", children: destructive?.issues.map((i, idx) => (_jsxs("li", { className: "border-l-2 border-amber-500 pl-2", children: [i.kind && _jsx(Badge, { variant: "outline", className: "mr-2", children: i.kind }), i.message ?? JSON.stringify(i)] }, idx))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setDestructive(null), children: t('engine.cancel') }), _jsxs(Button, { variant: "destructive", onClick: () => destructive && doSave(true, destructive.pending), disabled: saving, children: [saving && _jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), t('engine.edit.forceSave')] })] })] }) })] }));
|
|
268
269
|
}
|
|
269
270
|
function PermissionTable({ objects, draft, expanded, fieldsByObject, writable, objectActions, t, onToggleExpand, onObjectPerm, onFieldPerm, onBulkSet, }) {
|
|
270
271
|
return (_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "sticky top-0 bg-background border-b z-10", children: _jsxs("tr", { children: [_jsx("th", { className: "text-left px-4 py-2 font-medium w-72", children: t('perm.col.object') }), objectActions.map((a) => (_jsx("th", { className: "px-2 py-2 font-medium text-center w-14", title: a.tip, children: a.short }, a.key))), _jsx("th", { className: "px-2 py-2 font-medium w-44 text-right", children: t('perm.col.bulk') })] }) }), _jsxs("tbody", { children: [objects.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: objectActions.length + 2, className: "px-4 py-8 text-center text-muted-foreground", children: t('perm.filter.empty') }) })), objects.map((o) => {
|
|
@@ -49,6 +49,7 @@ import { detectLocale, t, tFormat, translateValidationMessage } from './i18n';
|
|
|
49
49
|
import { JsonSourceEditor } from './JsonSourceEditor';
|
|
50
50
|
import { validateMetadataDraft, hasClientValidator } from './clientValidation';
|
|
51
51
|
import { describeIssuePath } from './issuePath';
|
|
52
|
+
import { buildCreateModeBody } from './createBody';
|
|
52
53
|
// react-resizable-panels' `direction` prop type does not always narrow
|
|
53
54
|
// cleanly in our TS config; cast at the boundary (precedent:
|
|
54
55
|
// packages/components/src/custom/navigation-overlay.tsx).
|
|
@@ -311,6 +312,25 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
311
312
|
// Issues to DISPLAY (banner + inline). Suppressed on a pristine create form
|
|
312
313
|
// so a blank new item doesn't open covered in required-field errors.
|
|
313
314
|
const displayIssues = React.useMemo(() => (createMode && !createDirty ? [] : issues), [createMode, createDirty, issues]);
|
|
315
|
+
// Server-computed diagnostics handed to a canvas Preview (e.g. the flow
|
|
316
|
+
// Problems panel + on-canvas badges). Errors prefer the live client-side Zod
|
|
317
|
+
// issues when a client validator exists (so they track every keystroke);
|
|
318
|
+
// warnings stay server-sourced. Mirrors the read-only banner's source
|
|
319
|
+
// selection, flattened to a path-keyed, severity-tagged list.
|
|
320
|
+
const previewDiagnostics = React.useMemo(() => {
|
|
321
|
+
const diag = layered?._diagnostics;
|
|
322
|
+
const errs = hasClientValidator(type)
|
|
323
|
+
? displayIssues.map((i) => ({ path: i.path, message: translateValidationMessage(i.message, locale) }))
|
|
324
|
+
: (diag?.errors ?? []).map((i) => ({ path: i.path, message: translateValidationMessage(i.message, locale) }));
|
|
325
|
+
const warns = (diag?.warnings ?? []).map((i) => ({
|
|
326
|
+
path: i.path,
|
|
327
|
+
message: translateValidationMessage(i.message, locale),
|
|
328
|
+
}));
|
|
329
|
+
return [
|
|
330
|
+
...errs.map((e) => ({ path: e.path || undefined, message: e.message, severity: 'error' })),
|
|
331
|
+
...warns.map((w) => ({ path: w.path || undefined, message: w.message, severity: 'warning' })),
|
|
332
|
+
];
|
|
333
|
+
}, [layered, displayIssues, type, locale]);
|
|
314
334
|
// Per-item draft pending publish (mode=draft saves land here).
|
|
315
335
|
// When non-null, the editor is "viewing the draft" and we surface
|
|
316
336
|
// Publish / Discard-draft actions.
|
|
@@ -853,10 +873,14 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
853
873
|
// or `{ list: { data: { object } } }` for view) is present so
|
|
854
874
|
// the saved body satisfies its JSONSchema. User-supplied values
|
|
855
875
|
// always win over the defaults.
|
|
876
|
+
// Prefer the server's authoritative create seed (from /meta/types — the
|
|
877
|
+
// single source of truth in @objectstack/spec) over the locally hardcoded
|
|
878
|
+
// createDefaults, so the create shape can't drift from the spec's required
|
|
879
|
+
// fields (the dashboard-`layout` / action-`body` 422 family). `createSeed`
|
|
880
|
+
// is a runtime field absent from the bundled GetMetaTypes type, hence the cast.
|
|
881
|
+
const specCreateSeed = entry?.createSeed;
|
|
856
882
|
let builtBody = createMode
|
|
857
|
-
? (config
|
|
858
|
-
? config.createBuildBody(draft)
|
|
859
|
-
: { ...(config.createDefaults ?? {}), ...draft })
|
|
883
|
+
? buildCreateModeBody(config, draft, specCreateSeed)
|
|
860
884
|
// Edit mode: serialise the editor draft back to the wire shape
|
|
861
885
|
// (inverse of `toDraft` — e.g. `view` folds the `{ list | form }`
|
|
862
886
|
// family key back into the ViewItem `config` wrapper).
|
|
@@ -958,6 +982,14 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
958
982
|
setDestructiveIssues(Array.isArray(i) ? i : []);
|
|
959
983
|
setPendingItem(draft);
|
|
960
984
|
}
|
|
985
|
+
// ADR-0070 D1/D3 — the kernel rejects authoring into a read-only
|
|
986
|
+
// code/installed package (`writable_package_required`, also HTTP 422,
|
|
987
|
+
// so this MUST precede the generic invalid_metadata branch below).
|
|
988
|
+
// Surface an actionable message guiding the author to pick or create a
|
|
989
|
+
// writable base rather than mangling it into phantom field issues.
|
|
990
|
+
else if (err?.code === 'writable_package_required') {
|
|
991
|
+
setError(t('engine.package.writableRequired', locale));
|
|
992
|
+
}
|
|
961
993
|
// Map schema validation → inline field errors.
|
|
962
994
|
else if (err?.status === 422 || err?.code === 'invalid_metadata' || err?.code === 'invalid_payload') {
|
|
963
995
|
const i = err?.body?.issues ?? [];
|
|
@@ -1448,7 +1480,7 @@ function MetadataResourceEditPageImpl({ type, name, createMode, embedded, }) {
|
|
|
1448
1480
|
? t('engine.edit.exitFullscreen', locale)
|
|
1449
1481
|
: t('engine.edit.fullscreen', locale), children: isFullscreen ? (_jsx(Minimize2, { className: "h-3.5 w-3.5" })) : (_jsx(Maximize2, { className: "h-3.5 w-3.5" })) })] })] }), _jsx("div", { className: "flex-1 min-h-0 overflow-auto p-4 bg-[radial-gradient(circle_at_1px_1px,theme(colors.border)_1px,transparent_0)] [background-size:16px_16px] bg-muted/30", children: _jsx(PreviewComponent, { type: type, name: name, draft: draft, baseline: !createMode
|
|
1450
1482
|
? (layered?.effective ?? undefined)
|
|
1451
|
-
: undefined, editing: editing && !previewOnly, selection: previewOnly ? null : selection, onSelectionChange: setSelection, locale: locale, onPatch: (patch) => handleDraftChange((d) => ({ ...d, ...patch })) }) })] }) }), _jsx(ResizableHandle, { withHandle: true, className: inspectorCollapsed
|
|
1483
|
+
: undefined, editing: editing && !previewOnly, selection: previewOnly ? null : selection, onSelectionChange: setSelection, locale: locale, diagnostics: previewDiagnostics, onPatch: (patch) => handleDraftChange((d) => ({ ...d, ...patch })) }) })] }) }), _jsx(ResizableHandle, { withHandle: true, className: inspectorCollapsed
|
|
1452
1484
|
? 'hidden'
|
|
1453
1485
|
: 'w-1.5 bg-border/40 hover:bg-primary/40 active:bg-primary/60 transition-colors' }), _jsx(ResizablePanel, { panelRef: inspectorPanelRef, defaultSize: lastInspectorSizeRef.current, minSize: 22, collapsible: true, collapsedSize: 0, onResize: (size) => {
|
|
1454
1486
|
const pct = size.asPercentage;
|
|
@@ -21,10 +21,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '
|
|
|
21
21
|
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
22
22
|
import { PageShell } from './PageShell';
|
|
23
23
|
import { MetadataTypeActions } from './MetadataTypeActions';
|
|
24
|
+
import { CreatePackageDialog } from './PackagesPage';
|
|
24
25
|
import { useMetadataClient, useMetadataTypes, matchesQuery, } from './useMetadata';
|
|
25
26
|
import { getMetadataResource, resolveResourceConfig, } from './registry';
|
|
26
27
|
import { t, tFormat, translateMetadataType, detectLocale } from './i18n';
|
|
27
|
-
import { buildPackageScopeOptions, LOCAL_PACKAGE_ID } from './package-scope';
|
|
28
|
+
import { buildPackageScopeOptions, LOCAL_PACKAGE_ID, isLocalScope } from './package-scope';
|
|
28
29
|
/**
|
|
29
30
|
* Derive provenance from item._packageId. The `loadMetaFromDb` path
|
|
30
31
|
* tags objects with the synthetic packageId 'sys_metadata' (see
|
|
@@ -184,6 +185,22 @@ function DefaultMetadataList({ type, appName }) {
|
|
|
184
185
|
const pkgSuffix = activePackage
|
|
185
186
|
? `?package=${encodeURIComponent(activePackage)}`
|
|
186
187
|
: '';
|
|
188
|
+
// ADR-0070 D3 — never start a create that would orphan the item. When a real
|
|
189
|
+
// writable base exists, create into it (defaulting away from the Local/null
|
|
190
|
+
// scope); when none exists yet, prompt to create a base first.
|
|
191
|
+
const [showCreateBase, setShowCreateBase] = React.useState(false);
|
|
192
|
+
const handleCreate = React.useCallback(() => {
|
|
193
|
+
const realBases = (projectPackages ?? []).filter((p) => !isLocalScope(p.id));
|
|
194
|
+
if (projectPackages !== null && realBases.length === 0) {
|
|
195
|
+
setShowCreateBase(true);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (realBases.length > 0 && (!activePackage || isLocalScope(activePackage))) {
|
|
199
|
+
navigate(`./new?package=${encodeURIComponent(realBases[0].id)}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
navigate(`./new${pkgSuffix}`);
|
|
203
|
+
}, [projectPackages, activePackage, pkgSuffix, navigate]);
|
|
187
204
|
React.useEffect(() => {
|
|
188
205
|
let cancelled = false;
|
|
189
206
|
setLoading(true);
|
|
@@ -295,14 +312,14 @@ function DefaultMetadataList({ type, appName }) {
|
|
|
295
312
|
},
|
|
296
313
|
]
|
|
297
314
|
: []),
|
|
298
|
-
], actions: _jsxs(_Fragment, { children: [_jsx(MetadataTypeActions, { entry: entry, location: "list_toolbar", onAfter: () => setRefreshKey((k) => k + 1) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), title: t('engine.list.refresh', locale), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), (entry?.allowOrgOverride || entry?.allowRuntimeCreate) && (_jsxs(Button, { size: "sm", variant: config.createFields ? 'default' : 'outline', onClick:
|
|
315
|
+
], actions: _jsxs(_Fragment, { children: [_jsx(MetadataTypeActions, { entry: entry, location: "list_toolbar", onAfter: () => setRefreshKey((k) => k + 1) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), title: t('engine.list.refresh', locale), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), (entry?.allowOrgOverride || entry?.allowRuntimeCreate) && (_jsxs(Button, { size: "sm", variant: config.createFields ? 'default' : 'outline', onClick: handleCreate, title: config.createFields
|
|
299
316
|
? tFormat('engine.list.createHint', locale, { type: typeLabel })
|
|
300
|
-
: undefined, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), t('engine.list.create', locale)] }))] }), children: _jsxs("div", { className: "p-6 space-y-4", children: [_jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[200px] max-w-md", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }), _jsx(Input, { className: "pl-8", placeholder: t('engine.list.search', locale), value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsxs(Select, { value: sourceFilter, onValueChange: setSourceFilter, children: [_jsx(SelectTrigger, { className: "w-[180px]", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsxs(SelectItem, { value: "all", children: [t('engine.list.allSources', locale), " (", sourceCounts.all, ")"] }), _jsxs(SelectItem, { value: "artifact", children: [t('engine.list.source.artifact', locale), " (", sourceCounts.artifact, ")"] }), _jsxs(SelectItem, { value: "runtime", children: [t('engine.list.source.runtime', locale), " (", sourceCounts.runtime, ")"] })] })] })] }), (loading || projectPackages === null) && (_jsxs("div", { className: "text-sm text-muted-foreground", children: [t('engine.edit.loading', locale), " ", type, "\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && projectPackages !== null && projectPackages.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No project packages installed" }), _jsx(EmptyDescription, { children: "Studio only shows metadata that belongs to a project software package. Install or create a project package to manage its metadata here." })] })), !loading && !error && projectPackages !== null && projectPackages.length > 0 && filtered.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: scopedItems.length === 0
|
|
317
|
+
: undefined, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), t('engine.list.create', locale)] }))] }), children: _jsxs("div", { className: "p-6 space-y-4", children: [_jsx(CreatePackageDialog, { open: showCreateBase, onOpenChange: setShowCreateBase, onCreated: (id) => navigate(`./new?package=${encodeURIComponent(id)}`) }), _jsxs("div", { className: "flex items-center gap-3 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[200px] max-w-md", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }), _jsx(Input, { className: "pl-8", placeholder: t('engine.list.search', locale), value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsxs(Select, { value: sourceFilter, onValueChange: setSourceFilter, children: [_jsx(SelectTrigger, { className: "w-[180px]", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsxs(SelectItem, { value: "all", children: [t('engine.list.allSources', locale), " (", sourceCounts.all, ")"] }), _jsxs(SelectItem, { value: "artifact", children: [t('engine.list.source.artifact', locale), " (", sourceCounts.artifact, ")"] }), _jsxs(SelectItem, { value: "runtime", children: [t('engine.list.source.runtime', locale), " (", sourceCounts.runtime, ")"] })] })] })] }), (loading || projectPackages === null) && (_jsxs("div", { className: "text-sm text-muted-foreground", children: [t('engine.edit.loading', locale), " ", type, "\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && projectPackages !== null && projectPackages.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No project packages installed" }), _jsx(EmptyDescription, { children: "Studio only shows metadata that belongs to a project software package. Install or create a project package to manage its metadata here." })] })), !loading && !error && projectPackages !== null && projectPackages.length > 0 && filtered.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: scopedItems.length === 0
|
|
301
318
|
? tFormat('engine.list.emptyType', locale, { type: typeLabel })
|
|
302
319
|
: tFormat('engine.list.emptyQuery', locale, { query }) }), _jsx(EmptyDescription, { children: config.emptyStateHint ??
|
|
303
320
|
(entry?.allowOrgOverride || entry?.allowRuntimeCreate
|
|
304
321
|
? tFormat('engine.list.createHint', locale, { type: typeLabel })
|
|
305
|
-
: t('engine.list.readOnlyHint', locale)) }), scopedItems.length === 0 && (entry?.allowOrgOverride || entry?.allowRuntimeCreate) && (_jsx("div", { className: "mt-4", children: _jsxs(Button, { onClick:
|
|
322
|
+
: t('engine.list.readOnlyHint', locale)) }), scopedItems.length === 0 && (entry?.allowOrgOverride || entry?.allowRuntimeCreate) && (_jsx("div", { className: "mt-4", children: _jsxs(Button, { onClick: handleCreate, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), t('engine.list.create', locale)] }) }))] })), !loading && filtered.length > 0 && (_jsx("div", { className: "border rounded-lg overflow-hidden", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-xs uppercase tracking-wider text-muted-foreground", children: _jsxs("tr", { children: [columns.map((c) => (_jsx("th", { className: "px-3 py-2 text-left font-medium", style: c.width ? { width: c.width } : undefined, children: localizeColumnLabel(c) }, c.key))), _jsx("th", { className: "px-3 py-2 text-right font-medium w-[80px]", children: t('engine.list.col.source', locale) })] }) }), _jsx("tbody", { className: "divide-y", children: filtered.map((row, i) => {
|
|
306
323
|
const pk = config.primaryKey ?? 'name';
|
|
307
324
|
const name = String(row.item[pk] ?? `(unnamed-${i})`);
|
|
308
325
|
// ADR-0048 — link to this row's OWNING package so the editor
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
import type { MetadataResourceConfig } from './registry';
|
|
9
|
+
/**
|
|
10
|
+
* Build the create-mode save body for a new metadata item.
|
|
11
|
+
*
|
|
12
|
+
* Prefers the server's authoritative **create seed** (delivered per type on the
|
|
13
|
+
* `/meta/types` registry entry — the single source of truth in
|
|
14
|
+
* `@objectstack/spec`) over the locally hardcoded `createDefaults`. This is the
|
|
15
|
+
* drift-stop for the recurring "the designer emits a minimal shape the spec
|
|
16
|
+
* rejects, so create→save 422s" family (dashboard `layout`, action `body`):
|
|
17
|
+
* the structural defaults now come from the same place the spec validates
|
|
18
|
+
* against, so they cannot diverge. Falls back to `createDefaults` when the
|
|
19
|
+
* server provides no seed (older server, or canvas-create types whose shape is
|
|
20
|
+
* built interactively).
|
|
21
|
+
*
|
|
22
|
+
* User-supplied draft values always win over the seed's placeholders.
|
|
23
|
+
* `createBuildBody` (dynamic identity, e.g. a view's qualified name) still takes
|
|
24
|
+
* precedence — it incorporates user input the static seed cannot.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildCreateModeBody(config: Pick<MetadataResourceConfig, 'createBuildBody' | 'createDefaults'>, draft: Record<string, unknown>, specCreateSeed: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
* Build the create-mode save body for a new metadata item.
|
|
10
|
+
*
|
|
11
|
+
* Prefers the server's authoritative **create seed** (delivered per type on the
|
|
12
|
+
* `/meta/types` registry entry — the single source of truth in
|
|
13
|
+
* `@objectstack/spec`) over the locally hardcoded `createDefaults`. This is the
|
|
14
|
+
* drift-stop for the recurring "the designer emits a minimal shape the spec
|
|
15
|
+
* rejects, so create→save 422s" family (dashboard `layout`, action `body`):
|
|
16
|
+
* the structural defaults now come from the same place the spec validates
|
|
17
|
+
* against, so they cannot diverge. Falls back to `createDefaults` when the
|
|
18
|
+
* server provides no seed (older server, or canvas-create types whose shape is
|
|
19
|
+
* built interactively).
|
|
20
|
+
*
|
|
21
|
+
* User-supplied draft values always win over the seed's placeholders.
|
|
22
|
+
* `createBuildBody` (dynamic identity, e.g. a view's qualified name) still takes
|
|
23
|
+
* precedence — it incorporates user input the static seed cannot.
|
|
24
|
+
*/
|
|
25
|
+
export function buildCreateModeBody(config, draft, specCreateSeed) {
|
|
26
|
+
if (config.createBuildBody) {
|
|
27
|
+
return config.createBuildBody(draft);
|
|
28
|
+
}
|
|
29
|
+
return { ...(specCreateSeed ?? config.createDefaults ?? {}), ...draft };
|
|
30
|
+
}
|
|
@@ -182,6 +182,7 @@ const ENGINE_STRINGS_EN = {
|
|
|
182
182
|
'engine.list.allSources': 'All sources',
|
|
183
183
|
'engine.list.allPackages': 'All packages',
|
|
184
184
|
'engine.package.local': 'Local / Custom (this env)',
|
|
185
|
+
'engine.package.writableRequired': 'Pick or create a writable base (package) first — this item cannot be authored into a read-only code package.',
|
|
185
186
|
'engine.list.packageFilter': 'Package',
|
|
186
187
|
'engine.list.source.artifact': 'Artifact',
|
|
187
188
|
'engine.list.source.runtime': 'Runtime',
|
|
@@ -599,6 +600,15 @@ const ENGINE_STRINGS_EN = {
|
|
|
599
600
|
'engine.packages.detail.disabled': 'Package disabled.',
|
|
600
601
|
'engine.packages.detail.enabled': 'Package enabled.',
|
|
601
602
|
'engine.packages.detail.exported': 'Package exported.',
|
|
603
|
+
'engine.packages.detail.duplicate': 'Duplicate',
|
|
604
|
+
'engine.packages.detail.duplicating': 'Duplicating…',
|
|
605
|
+
'engine.packages.detail.duplicatePrompt': 'New package id for the duplicate (a fresh writable base):',
|
|
606
|
+
'engine.packages.detail.duplicated': 'Package duplicated into a new base.',
|
|
607
|
+
'engine.packages.detail.adoptOrphans': 'Adopt loose items',
|
|
608
|
+
'engine.packages.detail.adopting': 'Adopting…',
|
|
609
|
+
'engine.packages.detail.adoptConfirm': 'Move all package-less (loose) metadata in this environment INTO "{name}"? This rebinds orphaned items to this base.',
|
|
610
|
+
'engine.packages.detail.adopted': 'Loose items adopted into this base.',
|
|
611
|
+
'engine.packages.detail.deleteKeepData': 'Delete the DATA too?\n\nOK = also drop all records (destructive). Cancel = keep records, delete only the structure.',
|
|
602
612
|
'engine.quickfind.placeholder': "Find metadata types or items… (try 'view', 'account')",
|
|
603
613
|
'engine.quickfind.empty': 'Type to search across all metadata types.',
|
|
604
614
|
'engine.quickfind.title': 'Quick Find',
|
|
@@ -798,6 +808,7 @@ const ENGINE_STRINGS_EN = {
|
|
|
798
808
|
'designer.canvas.askAiGenerate': 'Generate fields with AI',
|
|
799
809
|
};
|
|
800
810
|
const ENGINE_STRINGS_ZH = {
|
|
811
|
+
'engine.package.writableRequired': '请先选择或新建一个可写的基座(package)——只读的代码包中无法新建该项。',
|
|
801
812
|
'engine.directory.title': '元数据',
|
|
802
813
|
'engine.directory.description': '平台协议共暴露 {count} 个元数据类型(其中 {writable} 个支持运行时覆盖)。点击任意卡片即可浏览、覆盖或创建实例。',
|
|
803
814
|
'engine.directory.search': '搜索元数据类型…',
|
|
@@ -1284,6 +1295,15 @@ const ENGINE_STRINGS_ZH = {
|
|
|
1284
1295
|
'engine.packages.detail.disabled': '软件包已禁用。',
|
|
1285
1296
|
'engine.packages.detail.enabled': '软件包已启用。',
|
|
1286
1297
|
'engine.packages.detail.exported': '软件包已导出。',
|
|
1298
|
+
'engine.packages.detail.duplicate': '复制',
|
|
1299
|
+
'engine.packages.detail.duplicating': '复制中…',
|
|
1300
|
+
'engine.packages.detail.duplicatePrompt': '副本的新软件包 id(一个全新的可写基座):',
|
|
1301
|
+
'engine.packages.detail.duplicated': '软件包已复制为新基座。',
|
|
1302
|
+
'engine.packages.detail.adoptOrphans': '收编散落项',
|
|
1303
|
+
'engine.packages.detail.adopting': '收编中…',
|
|
1304
|
+
'engine.packages.detail.adoptConfirm': '把本环境中所有无软件包(散落)的元数据移动到 "{name}" 吗?这会把孤儿项重新绑定到此基座。',
|
|
1305
|
+
'engine.packages.detail.adopted': '散落项已收编进此基座。',
|
|
1306
|
+
'engine.packages.detail.deleteKeepData': '同时删除数据吗?\n\n确定 = 同时删除所有记录(破坏性)。取消 = 保留记录,仅删除结构。',
|
|
1287
1307
|
'engine.quickfind.placeholder': '搜索元数据类型或条目…(如:view、account)',
|
|
1288
1308
|
'engine.quickfind.empty': '输入关键字以搜索所有元数据类型。',
|
|
1289
1309
|
'engine.quickfind.title': '快速查找',
|
|
@@ -18,4 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as React from 'react';
|
|
20
20
|
import type { MetadataDefaultInspectorProps } from '../default-inspector-registry';
|
|
21
|
+
/**
|
|
22
|
+
* Patch for a base-object change. A dataset's joins (`include`), `dimensions`,
|
|
23
|
+
* `measures`, and `filter` all reference the OLD object's fields, so a real
|
|
24
|
+
* object change re-bases the dataset and clears them — preventing stale field
|
|
25
|
+
* refs from silently producing broken/ambiguous queries. Selecting the SAME
|
|
26
|
+
* object is a no-op (only sets `object`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function objectChangePatch(next: string, current: string): Record<string, unknown>;
|
|
21
29
|
export declare function DatasetDefaultInspector({ draft, onPatch, readOnly, name }: MetadataDefaultInspectorProps): React.JSX.Element;
|
|
@@ -123,11 +123,13 @@ function MeasureFormatField({ measure, onPatch, disabled }) {
|
|
|
123
123
|
const sample = formatMeasure(kind === 'percent' ? 0.1234 : 1234.5, measure.format, measure.currency);
|
|
124
124
|
return (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "grid grid-cols-2 gap-1.5", children: [_jsx(InspectorSelectField, { label: "Display format", value: kind, options: FORMAT_KIND_OPTIONS, onCommit: (v) => apply(v, decimals, currency), disabled: disabled }), kind !== 'raw' && (_jsx(InspectorSelectField, { label: "Decimals", value: String(decimals), options: DECIMALS_OPTIONS, onCommit: (v) => apply(kind, parseInt(v, 10) || 0, currency), disabled: disabled }))] }), kind === 'currency' && (_jsx(InspectorSelectField, { label: "Currency", value: currency, options: CURRENCY_OPTIONS, onCommit: (v) => apply(kind, decimals, v), disabled: disabled })), kind !== 'raw' && (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: ["Sample: ", _jsx("span", { className: "font-mono tabular-nums", children: sample })] }))] }));
|
|
125
125
|
}
|
|
126
|
-
/** The relationship
|
|
126
|
+
/** The relationship PATH of a `relationship[.relationship].field` reference (all
|
|
127
|
+
* segments but the final column) that isn't yet in `include`, else null. ADR-0071
|
|
128
|
+
* multi-hop: `account.owner.region` → `account.owner`. */
|
|
127
129
|
function missingRelationship(field, include) {
|
|
128
130
|
if (!field || !field.includes('.'))
|
|
129
131
|
return null;
|
|
130
|
-
const rel = field.
|
|
132
|
+
const rel = field.slice(0, field.lastIndexOf('.'));
|
|
131
133
|
return rel && !include.includes(rel) ? rel : null;
|
|
132
134
|
}
|
|
133
135
|
/** Inline author-time warning: a `relationship.field` whose join isn't declared in `include`. */
|
|
@@ -146,6 +148,18 @@ function DatasetFilterField({ label, help, value, onCommit, fields, disabled })
|
|
|
146
148
|
const count = group.conditions.length;
|
|
147
149
|
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), !representable ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-2.5 py-1.5 text-[11px] text-muted-foreground", children: ["Advanced filter (nested / OR) \u2014 edit it in the ", _jsx("span", { className: "font-medium", children: "Source" }), " tab."] })) : (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled: disabled, className: "h-8 w-full justify-between text-xs font-normal", children: [_jsx("span", { className: "truncate text-left", children: count ? `${count} condition${count === 1 ? '' : 's'}` : _jsx("span", { className: "text-muted-foreground", children: "+ Add filter\u2026" }) }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 opacity-60 shrink-0" })] }) }), _jsx(PopoverContent, { align: "start", className: "w-[440px] max-w-[90vw] p-3", children: fields.length === 0 ? (_jsx("p", { className: "text-xs text-muted-foreground", children: "Pick a base object to add filter conditions." })) : (_jsx(FilterBuilder, { fields: fields, value: group, onChange: (g) => onCommit(groupToCondition(g)) })) })] })), help && _jsx("p", { className: "text-[10px] text-muted-foreground", children: help })] }));
|
|
148
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Patch for a base-object change. A dataset's joins (`include`), `dimensions`,
|
|
153
|
+
* `measures`, and `filter` all reference the OLD object's fields, so a real
|
|
154
|
+
* object change re-bases the dataset and clears them — preventing stale field
|
|
155
|
+
* refs from silently producing broken/ambiguous queries. Selecting the SAME
|
|
156
|
+
* object is a no-op (only sets `object`).
|
|
157
|
+
*/
|
|
158
|
+
export function objectChangePatch(next, current) {
|
|
159
|
+
if (next === current)
|
|
160
|
+
return { object: next };
|
|
161
|
+
return { object: next, include: [], dimensions: [], measures: [], filter: undefined };
|
|
162
|
+
}
|
|
149
163
|
export function DatasetDefaultInspector({ draft, onPatch, readOnly, name }) {
|
|
150
164
|
const label = typeof draft.label === 'string' ? draft.label : '';
|
|
151
165
|
const description = typeof draft.description === 'string' ? draft.description : '';
|
|
@@ -203,7 +217,7 @@ export function DatasetDefaultInspector({ draft, onPatch, readOnly, name }) {
|
|
|
203
217
|
if (createMode && !nameTouched.current)
|
|
204
218
|
patch.name = toFieldName(v);
|
|
205
219
|
onPatch(patch);
|
|
206
|
-
}, disabled: readOnly }), _jsx(InspectorTextField, { label: "Description", value: description, onCommit: (v) => onPatch({ description: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Base object", value: object, onCommit: (v) => onPatch(
|
|
220
|
+
}, disabled: readOnly }), _jsx(InspectorTextField, { label: "Description", value: description, onCommit: (v) => onPatch({ description: v }), disabled: readOnly }), _jsx(InspectorComboField, { label: "Base object", value: object, onCommit: (v) => onPatch(objectChangePatch(v, object)), options: objectComboOptions, loading: objectsLoading, placeholder: "Select an object\u2026", searchPlaceholder: "Search objects\u2026", disabled: readOnly, mono: true }), object && (dimensions.length > 0 || measures.length > 0 || include.length > 0 || !!datasetFilter) && (_jsx("p", { className: "text-[10px] text-muted-foreground", children: "Changing the base object clears its dimensions, measures, joins & filters." })), _jsxs("div", { className: "border-t pt-3 space-y-1.5", children: [_jsx(SectionHeader, { title: "Included relationships", count: include.length, addLabel: "Add", onAdd: readOnly ? undefined : () => onPatch({ include: appendArray(include, '') }) }), include.length === 0 ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/30 px-3 py-2 text-center text-[11px] text-muted-foreground", children: ["No joins. Add a relationship (a lookup field on ", _jsx("code", { children: baseLabel || 'the base object' }), ") to use ", _jsx("code", { children: "relationship.field" }), " dimensions/measures."] })) : (include.map((rel, i) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(InspectorComboField, { value: rel, onCommit: (v) => onPatch({ include: include.map((r, idx) => (idx === i ? v : r)) }), options: relationshipComboOptions, loading: catalogLoading, placeholder: "Select a relationship\u2026", searchPlaceholder: "Search relationships\u2026", disabled: readOnly, mono: true }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 w-7 shrink-0 p-0", onClick: () => onPatch({ include: spliceArray(include, i, null) }), "aria-label": "Remove relationship", children: _jsx(X, { className: "h-3.5 w-3.5" }) }))] }, i)))), object && include.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 gap-y-0.5 pt-0.5 text-[10px] text-muted-foreground", children: [_jsx("span", { className: "font-mono font-medium", children: baseLabel }), include.map((rel, i) => {
|
|
207
221
|
const r = relationships.find((x) => x.name === rel);
|
|
208
222
|
return (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(ArrowRight, { className: "h-3 w-3 opacity-60" }), _jsxs("span", { className: "font-mono", children: [rel, r?.referenceTo ? ` (${r.referenceTo})` : ''] })] }, i));
|
|
209
223
|
})] }))] }), _jsx("div", { className: "border-t pt-3", children: _jsx(DatasetFilterField, { label: "Scope filter", help: "Intrinsic scope, ANDed into every query (e.g. exclude soft-deleted records).", value: datasetFilter, onCommit: (fc) => onPatch({ filter: fc }), fields: filterFields, disabled: readOnly }) }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Dimensions", count: dimensions.length, addLabel: "Add dimension", onAdd: readOnly ? undefined : () => onPatch({ dimensions: appendArray(dimensions, { name: '', field: '', type: 'string' }) }) }), dimensions.map((d, i) => (_jsxs("div", { className: "rounded-md border p-2 space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-[11px] font-medium text-muted-foreground", children: ["Dimension ", i + 1] }), !readOnly && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Remove dimension", title: "Remove dimension", className: "h-6 w-6 shrink-0 p-0 text-muted-foreground hover:text-destructive", onClick: () => onPatch({ dimensions: spliceArray(dimensions, i, null) }), children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }))] }), _jsx(InspectorTextField, { label: "Name", value: d.name ?? '', onCommit: (v) => patchDimension(i, { name: v }), placeholder: "e.g. region", disabled: readOnly, mono: true }), _jsx(InspectorComboField, { label: "Field", value: d.field ?? '', onCommit: (v) => pickDimensionField(i, v), options: fieldComboOptions, loading: catalogLoading, placeholder: "field or relationship.field", searchPlaceholder: "Search fields\u2026", disabled: readOnly, mono: true }), (() => { const rel = missingRelationship(d.field, include); return rel ? _jsx(RelWarning, { rel: rel, disabled: readOnly, onAdd: () => onPatch({ include: appendArray(include, rel) }) }) : null; })(), _jsx(InspectorSelectField, { label: "Type", value: d.type, options: DIMENSION_TYPE_OPTIONS, onCommit: (v) => patchDimension(i, { type: v }), disabled: readOnly }), _jsxs(Advanced, { children: [_jsx(InspectorTextField, { label: "Label (optional)", value: d.label ?? '', onCommit: (v) => patchDimension(i, { label: v || undefined }), placeholder: d.name || 'Display label', disabled: readOnly }), d.type === 'date' && (_jsx(InspectorSelectField, { label: "Date bucket", value: d.dateGranularity ?? '', options: DATE_GRANULARITY_OPTIONS, onCommit: (v) => patchDimension(i, { dateGranularity: v || undefined }), disabled: readOnly }))] })] }, i)))] }), _jsxs("div", { className: "border-t pt-3 space-y-2", children: [_jsx(SectionHeader, { title: "Measures", count: measures.length, addLabel: "Add measure", onAdd: readOnly ? undefined : () => onPatch({ measures: appendArray(measures, { name: '', aggregate: 'sum', field: '' }) }) }), measures.map((m, i) => {
|
|
@@ -4,6 +4,9 @@ import { InspectorShell, InspectorTextField, InspectorSelectField, InspectorChec
|
|
|
4
4
|
import { Label } from '@object-ui/components';
|
|
5
5
|
import { edgeKey, conditionText } from '../previews/flow-canvas-layout';
|
|
6
6
|
import { validateExpressionClient } from './expression-validate';
|
|
7
|
+
import { useFlowScope } from './useFlowScope';
|
|
8
|
+
import { VariableTextInput } from './VariableTextInput';
|
|
9
|
+
import { findUnknownRefs, scopeRoots, describeUnknownRefs } from './flow-ref-check';
|
|
7
10
|
/** Read-only display of an edge endpoint (source / target node id). */
|
|
8
11
|
function EndpointRow({ label, value }) {
|
|
9
12
|
return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: label }), _jsx("div", { className: "flex h-8 items-center rounded border bg-muted/30 px-2 font-mono text-sm text-muted-foreground", children: value })] }));
|
|
@@ -12,6 +15,9 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
12
15
|
const edges = Array.isArray(draft.edges) ? draft.edges : [];
|
|
13
16
|
const index = edges.findIndex((e, i) => edgeKey(e, i) === selection.id);
|
|
14
17
|
const edge = index >= 0 ? edges[index] : null;
|
|
18
|
+
// References available on this edge are those in scope at its SOURCE node
|
|
19
|
+
// (#1934). Called unconditionally — `edge?.source` is undefined when missing.
|
|
20
|
+
const { groups: scopeGroups } = useFlowScope(draft, edge?.source);
|
|
15
21
|
if (!edge) {
|
|
16
22
|
return (_jsx(InspectorShell, { kindLabel: t('engine.inspector.flowEdge.kind', locale), title: selection.label ?? selection.id, onClose: onClearSelection, closeLabel: t('engine.inspector.flowEdge.close', locale), children: _jsx(InspectorEmptyState, { message: t('engine.inspector.flowEdge.missing', locale) }) }));
|
|
17
23
|
}
|
|
@@ -109,11 +115,19 @@ export function FlowEdgeInspector({ selection, draft, onPatch, onClearSelection,
|
|
|
109
115
|
// Picking a branch writes the matching label; "Custom" keeps the
|
|
110
116
|
// free-text label the author typed below.
|
|
111
117
|
onCommit: (v) => { if (v)
|
|
112
|
-
patchEdge({ label: v }); }, disabled: readOnly })), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.label', locale), value: edge.label ?? '', onCommit: (v) => patchEdge({ label: v }), placeholder: t('engine.inspector.flowEdge.labelHint', locale), disabled: readOnly || isDefault }), _jsx(
|
|
118
|
+
patchEdge({ label: v }); }, disabled: readOnly })), _jsx(InspectorTextField, { label: t('engine.inspector.flowEdge.label', locale), value: edge.label ?? '', onCommit: (v) => patchEdge({ label: v }), placeholder: t('engine.inspector.flowEdge.labelHint', locale), disabled: readOnly || isDefault }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: t('engine.inspector.flowEdge.condition', locale) }), _jsx(VariableTextInput, { mode: "expression", mono: true, value: conditionText(edge.condition) ?? '', onValueChange: (v) => patchEdge({ condition: v || undefined }), groups: scopeGroups, placeholder: t('engine.inspector.flowEdge.conditionHint', locale), disabled: readOnly || isDefault })] }), (() => {
|
|
113
119
|
// ADR-0032 — flag a malformed edge guard (e.g. `{record.x}` brace-in-CEL)
|
|
114
120
|
// inline, with the same corrective message as build/agent validation.
|
|
115
121
|
const issue = isDefault ? null : validateExpressionClient('predicate', edge.condition);
|
|
116
|
-
|
|
122
|
+
if (issue) {
|
|
123
|
+
return (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: issue.message }));
|
|
124
|
+
}
|
|
125
|
+
// #1934 — gentle scope-aware "unknown reference" warning (refs in scope
|
|
126
|
+
// at the edge's SOURCE node), once the guard is structurally valid.
|
|
127
|
+
const unknown = isDefault
|
|
128
|
+
? []
|
|
129
|
+
: findUnknownRefs(conditionText(edge.condition), 'predicate', scopeRoots(scopeGroups.flatMap((g) => g.refs)));
|
|
130
|
+
return unknown.length > 0 ? (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: describeUnknownRefs(unknown) })) : null;
|
|
117
131
|
})(), _jsx(InspectorCheckboxField, { label: t('engine.inspector.flowEdge.isDefault', locale), value: isDefault,
|
|
118
132
|
// The default ("else") branch is taken when no other guard matches, so
|
|
119
133
|
// it carries neither a condition nor a branch label — clear both.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowExprIssue — the shared inline validation line for an expression / template
|
|
3
|
+
* value in the flow inspector (#1934). Renders, in precedence order:
|
|
4
|
+
* 1. an ADR-0032 brace/shape ERROR (red) — CEL fields only; a genuine template
|
|
5
|
+
* uses single-brace `{var}` legally, so the brace check never runs there;
|
|
6
|
+
* 2. else a scope-aware "unknown reference" WARNING (amber) — a referenced
|
|
7
|
+
* root not in scope at the node, with a "did you mean?" hint.
|
|
8
|
+
* Returns null when the value is clean (or scope is unknown). Used by the picker
|
|
9
|
+
* repeater cells (decision Branches, screen visibleWhen, key/value values) that
|
|
10
|
+
* carry the picker but otherwise had no inline validation.
|
|
11
|
+
*/
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
import { type ExprFieldRole } from './expression-validate';
|
|
14
|
+
import type { ScopeGroup } from './useFlowScope';
|
|
15
|
+
export interface FlowExprIssueProps {
|
|
16
|
+
value: unknown;
|
|
17
|
+
/** `'predicate'` / `'value'` → CEL (brace-checked); `'template'` → `{…}` holes. */
|
|
18
|
+
role: ExprFieldRole;
|
|
19
|
+
scopeGroups?: ScopeGroup[];
|
|
20
|
+
}
|
|
21
|
+
export declare function FlowExprIssue({ value, role, scopeGroups }: FlowExprIssueProps): React.ReactElement | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { validateExpressionClient } from './expression-validate';
|
|
3
|
+
import { findUnknownRefs, scopeRoots, describeUnknownRefs } from './flow-ref-check';
|
|
4
|
+
export function FlowExprIssue({ value, role, scopeGroups }) {
|
|
5
|
+
// Brace / shape error — CEL roles only (single-brace is valid in a template).
|
|
6
|
+
const issue = role === 'template' ? null : validateExpressionClient(role, value);
|
|
7
|
+
if (issue) {
|
|
8
|
+
return (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: issue.message }));
|
|
9
|
+
}
|
|
10
|
+
const roots = scopeGroups && scopeGroups.length > 0 ? scopeRoots(scopeGroups.flatMap((g) => g.refs)) : null;
|
|
11
|
+
const unknown = roots ? findUnknownRefs(value, role, roots) : [];
|
|
12
|
+
return unknown.length > 0 ? (_jsx("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", role: "note", children: describeUnknownRefs(unknown) })) : null;
|
|
13
|
+
}
|
|
@@ -15,10 +15,26 @@
|
|
|
15
15
|
* rows take precedence).
|
|
16
16
|
*/
|
|
17
17
|
import * as React from 'react';
|
|
18
|
+
import type { ScopeGroup } from './useFlowScope';
|
|
19
|
+
export interface Row {
|
|
20
|
+
id: string;
|
|
21
|
+
key: string;
|
|
22
|
+
/** Display string for the value cell. */
|
|
23
|
+
raw: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the stored value as `[key, value]` entries, accepting BOTH shapes a
|
|
27
|
+
* key/value config field can hold: the common object map (`{ var: value }`) and
|
|
28
|
+
* the assignment-node ARRAY form (`[{ variable|name|key, value }]`). The shape
|
|
29
|
+
* is preserved on write (see {@link rowsToValue}).
|
|
30
|
+
*/
|
|
31
|
+
export declare function toEntries(value: unknown): Array<[string, unknown]>;
|
|
32
|
+
/** Flush rows back to the SAME shape, skipping empty/duplicate keys (first wins). */
|
|
33
|
+
export declare function rowsToValue(rows: Row[], arrayShape: boolean): Record<string, unknown> | Array<Record<string, unknown>>;
|
|
18
34
|
export interface FlowKeyValueFieldProps {
|
|
19
35
|
label: string;
|
|
20
36
|
value: unknown;
|
|
21
|
-
onCommit: (value: Record<string, unknown> | undefined) => void;
|
|
37
|
+
onCommit: (value: Record<string, unknown> | Array<Record<string, unknown>> | undefined) => void;
|
|
22
38
|
disabled?: boolean;
|
|
23
39
|
help?: string;
|
|
24
40
|
addLabel: string;
|
|
@@ -26,5 +42,7 @@ export interface FlowKeyValueFieldProps {
|
|
|
26
42
|
valueLabel: string;
|
|
27
43
|
removeLabel: string;
|
|
28
44
|
emptyLabel: string;
|
|
45
|
+
/** In-scope variable references for the data-picker (#1934). */
|
|
46
|
+
scopeGroups?: ScopeGroup[];
|
|
29
47
|
}
|
|
30
|
-
export declare function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, }: FlowKeyValueFieldProps): React.JSX.Element;
|
|
48
|
+
export declare function FlowKeyValueField({ label, value, onCommit, disabled, help, addLabel, keyLabel, valueLabel, removeLabel, emptyLabel, scopeGroups, }: FlowKeyValueFieldProps): React.JSX.Element;
|