@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/dist/console/AppContent.js +9 -15
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +36 -9
  6. package/dist/console/home/HomeLayout.js +5 -7
  7. package/dist/console/home/HomePage.js +1 -9
  8. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  9. package/dist/console/organizations/OrganizationsPage.js +22 -3
  10. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  11. package/dist/console/organizations/provisionEnvironment.js +64 -0
  12. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  13. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  14. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  15. package/dist/environment/EnvironmentListToolbar.js +59 -0
  16. package/dist/environment/entitlements.d.ts +90 -0
  17. package/dist/environment/entitlements.js +91 -0
  18. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  19. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  20. package/dist/hooks/useActionModal.js +15 -1
  21. package/dist/hooks/useAiSurface.d.ts +59 -0
  22. package/dist/hooks/useAiSurface.js +78 -0
  23. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  24. package/dist/hooks/useConsoleActionRuntime.js +36 -8
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.js +5 -1
  27. package/dist/layout/AppHeader.js +28 -4
  28. package/dist/layout/ConsoleFloatingChatbot.js +16 -2
  29. package/dist/layout/ConsoleLayout.js +5 -6
  30. package/dist/preview/DraftPreviewBar.js +20 -7
  31. package/dist/providers/ExpressionProvider.js +9 -3
  32. package/dist/utils/index.d.ts +2 -2
  33. package/dist/utils/index.js +1 -1
  34. package/dist/utils/recordFormNavigation.d.ts +60 -0
  35. package/dist/utils/recordFormNavigation.js +35 -0
  36. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  37. package/dist/utils/resolvePageVarTokens.js +72 -0
  38. package/dist/views/CreateViewDialog.js +14 -1
  39. package/dist/views/ObjectView.js +26 -12
  40. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  41. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  42. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  43. package/dist/views/metadata-admin/PackagesPage.js +49 -4
  44. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  45. package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
  46. package/dist/views/metadata-admin/ResourceListPage.js +21 -4
  47. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  48. package/dist/views/metadata-admin/createBody.js +30 -0
  49. package/dist/views/metadata-admin/i18n.js +20 -0
  50. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
  51. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
  52. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
  53. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  54. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  55. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  56. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  57. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  58. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  59. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
  60. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  61. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  62. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  63. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  64. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
  65. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  66. package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
  67. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  68. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  69. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  70. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  71. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  72. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  73. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  74. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  75. package/dist/views/metadata-admin/package-scope.d.ts +15 -0
  76. package/dist/views/metadata-admin/package-scope.js +16 -0
  77. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  78. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
  79. package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
  80. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  81. package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
  82. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  83. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  84. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  85. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  86. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  87. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  88. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
  89. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  90. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  91. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  92. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  93. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
  94. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
  95. 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.createBuildBody
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: () => navigate(`./new${pkgSuffix}`), title: config.createFields
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: () => navigate(`./new${pkgSuffix}`), 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) => {
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 prefix of a `relationship.field` path that isn't yet in `include`, else null. */
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.split('.')[0];
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({ object: v }), options: objectComboOptions, loading: objectsLoading, placeholder: "Select an object\u2026", searchPlaceholder: "Search objects\u2026", disabled: readOnly, mono: true }), _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) => {
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(InspectorTextField, { label: t('engine.inspector.flowEdge.condition', locale), value: conditionText(edge.condition) ?? '', onCommit: (v) => patchEdge({ condition: v || undefined }), placeholder: t('engine.inspector.flowEdge.conditionHint', locale), disabled: readOnly || isDefault, mono: true }), (() => {
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
- return issue ? (_jsx("p", { className: "text-[11px] leading-snug text-destructive", role: "alert", children: issue.message })) : null;
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;