@object-ui/app-shell 7.1.0 → 7.3.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 (107) hide show
  1. package/CHANGELOG.md +320 -0
  2. package/dist/components/ManagedByBadge.js +1 -1
  3. package/dist/console/AppContent.js +9 -15
  4. package/dist/console/ConsoleShell.d.ts +16 -0
  5. package/dist/console/ConsoleShell.js +43 -2
  6. package/dist/console/ai/AiChatPage.js +64 -14
  7. package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
  8. package/dist/console/ai/BuildDebugDrawer.js +75 -0
  9. package/dist/console/ai/buildDebugApi.d.ts +94 -0
  10. package/dist/console/ai/buildDebugApi.js +16 -0
  11. package/dist/console/home/HomeLayout.js +5 -7
  12. package/dist/console/home/HomePage.js +1 -9
  13. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  14. package/dist/console/organizations/OrganizationsPage.js +32 -4
  15. package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
  16. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  17. package/dist/console/organizations/provisionEnvironment.js +64 -0
  18. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  19. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  20. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  21. package/dist/environment/EnvironmentListToolbar.js +59 -0
  22. package/dist/environment/entitlements.d.ts +90 -0
  23. package/dist/environment/entitlements.js +91 -0
  24. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  25. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  26. package/dist/hooks/useActionModal.js +15 -1
  27. package/dist/hooks/useAiSurface.d.ts +59 -0
  28. package/dist/hooks/useAiSurface.js +78 -0
  29. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  30. package/dist/hooks/useConsoleActionRuntime.js +36 -8
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +5 -1
  33. package/dist/layout/AppHeader.js +30 -5
  34. package/dist/layout/ConsoleFloatingChatbot.js +22 -4
  35. package/dist/layout/ConsoleLayout.js +5 -6
  36. package/dist/layout/ContextSelectors.js +0 -19
  37. package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
  38. package/dist/layout/WorkspaceSwitcher.js +76 -0
  39. package/dist/preview/DraftPreviewBar.js +20 -7
  40. package/dist/providers/ExpressionProvider.js +9 -3
  41. package/dist/utils/index.d.ts +2 -2
  42. package/dist/utils/index.js +1 -1
  43. package/dist/utils/managedByEmptyState.d.ts +1 -1
  44. package/dist/utils/managedByEmptyState.js +20 -2
  45. package/dist/utils/recordFormNavigation.d.ts +60 -0
  46. package/dist/utils/recordFormNavigation.js +35 -0
  47. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  48. package/dist/utils/resolvePageVarTokens.js +72 -0
  49. package/dist/views/CreateViewDialog.js +14 -1
  50. package/dist/views/ObjectView.js +27 -13
  51. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  52. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  53. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  54. package/dist/views/metadata-admin/PackagesPage.js +49 -4
  55. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  56. package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
  57. package/dist/views/metadata-admin/ResourceListPage.js +25 -10
  58. package/dist/views/metadata-admin/StudioHomePage.js +1 -5
  59. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  60. package/dist/views/metadata-admin/createBody.js +30 -0
  61. package/dist/views/metadata-admin/i18n.js +20 -2
  62. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
  63. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
  64. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
  65. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  66. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  67. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  68. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  69. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  70. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  71. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
  72. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  73. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  74. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  75. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  76. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
  77. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  78. package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
  79. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  80. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  81. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  82. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  83. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  84. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  85. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  86. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  87. package/dist/views/metadata-admin/package-scope.d.ts +9 -19
  88. package/dist/views/metadata-admin/package-scope.js +11 -25
  89. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  90. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
  91. package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
  92. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  93. package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
  94. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  95. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  96. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  97. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  98. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  99. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  100. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
  101. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  102. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  103. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  104. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  105. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
  106. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
  107. package/package.json +38 -38
@@ -0,0 +1,151 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * AssignedUsersSection — "Manage Assignments" for a permission set.
10
+ *
11
+ * The admin's mental model is "who holds this role / AI seat" — so this is a
12
+ * people-first list (name + email + remove), not a raw junction table. It reads
13
+ * `sys_user_permission_set` for the set, resolves each `user_id` to a real
14
+ * person, and uses the reusable `RecordPickerDialog` to assign more. Server-side
15
+ * rules on the junction insert (e.g. the AI-seat cap) are caught and shown as a
16
+ * friendly, localized inline message — not a raw developer error.
17
+ *
18
+ * Permission-set-agnostic: every role gets the same UI, and the AI seat
19
+ * (`ai_seat`) is just one of them. The generic add-by-picker engine (spec
20
+ * RecordRelatedListProps.add) powers the capability; this is the polished
21
+ * surface for the high-value case.
22
+ */
23
+ import * as React from 'react';
24
+ import { Button } from '@object-ui/components';
25
+ import { RecordPickerDialog } from '@object-ui/fields';
26
+ import { useAdapter } from '@object-ui/react';
27
+ import { Plus, X, Users, Loader2, AlertCircle } from 'lucide-react';
28
+ import { detectLocale } from './i18n';
29
+ /** Minimal locale-aware copy (zh vs everything-else) — keeps the surface in the user's language. */
30
+ function useCopy() {
31
+ const zh = React.useMemo(() => detectLocale().toLowerCase().startsWith('zh'), []);
32
+ return React.useMemo(() => zh
33
+ ? {
34
+ title: '已分配用户',
35
+ add: '添加用户',
36
+ remove: '移除',
37
+ empty: '还没有分配任何用户。点击「添加用户」来分配。',
38
+ loading: '加载中…',
39
+ pickTitle: '选择要分配的用户',
40
+ seatFull: (n) => 'AI 席位已用完(' + n + '/' + n + ')。请先移除一个用户,或在许可证中提升席位上限,再分配新用户。',
41
+ addFailed: '分配失败,请重试。',
42
+ countOf: (n) => n + ' 人',
43
+ }
44
+ : {
45
+ title: 'Assigned Users',
46
+ add: 'Add user',
47
+ remove: 'Remove',
48
+ empty: 'No users assigned yet. Click "Add user" to assign.',
49
+ loading: 'Loading…',
50
+ pickTitle: 'Select users to assign',
51
+ seatFull: (n) => 'All ' + n + ' AI seat(s) are in use. Remove a user or raise the license cap before assigning another.',
52
+ addFailed: 'Failed to assign. Please try again.',
53
+ countOf: (n) => String(n),
54
+ }, [zh]);
55
+ }
56
+ const asArray = (res) => Array.isArray(res) ? res : res?.records ?? res?.items ?? res?.data ?? [];
57
+ const personLabel = (u) => u?.full_name || u?.name || u?.display_name || u?.email || String(u?.id ?? '');
58
+ export function AssignedUsersSection({ permissionSetName }) {
59
+ const adapter = useAdapter();
60
+ const c = useCopy();
61
+ const [setId, setSetId] = React.useState(null);
62
+ const [rows, setRows] = React.useState([]);
63
+ const [loading, setLoading] = React.useState(true);
64
+ const [pickerOpen, setPickerOpen] = React.useState(false);
65
+ const [busy, setBusy] = React.useState(false);
66
+ const [error, setError] = React.useState(null);
67
+ const load = React.useCallback(async () => {
68
+ setLoading(true);
69
+ try {
70
+ const sets = asArray(await adapter.find('sys_permission_set', { $filter: { name: permissionSetName }, limit: 1 }));
71
+ const id = sets[0]?.id ? String(sets[0].id) : null;
72
+ setSetId(id);
73
+ if (!id) {
74
+ setRows([]);
75
+ return;
76
+ }
77
+ const grants = asArray(await adapter.find('sys_user_permission_set', { $filter: { permission_set_id: id }, $top: 500 }));
78
+ const userIds = [...new Set(grants.map((g) => g.user_id).filter(Boolean).map(String))];
79
+ const users = userIds.length
80
+ ? asArray(await adapter.find('sys_user', { $filter: { id: { $in: userIds } }, $top: 500 }))
81
+ : [];
82
+ const byId = new Map(users.map((u) => [String(u.id), u]));
83
+ setRows(grants
84
+ .filter((g) => g.user_id)
85
+ .map((g) => {
86
+ const u = byId.get(String(g.user_id));
87
+ return {
88
+ grantId: String(g.id),
89
+ userId: String(g.user_id),
90
+ name: u ? personLabel(u) : String(g.user_id),
91
+ email: u?.email ?? '',
92
+ };
93
+ }));
94
+ }
95
+ catch {
96
+ setRows([]);
97
+ }
98
+ finally {
99
+ setLoading(false);
100
+ }
101
+ }, [adapter, permissionSetName]);
102
+ React.useEffect(() => {
103
+ void load();
104
+ }, [load]);
105
+ const assignedIds = React.useMemo(() => new Set(rows.map((r) => r.userId)), [rows]);
106
+ const addUsers = React.useCallback(async (records) => {
107
+ if (!setId)
108
+ return;
109
+ setBusy(true);
110
+ setError(null);
111
+ try {
112
+ for (const u of records || []) {
113
+ const uid = u?.id != null ? String(u.id) : null;
114
+ if (!uid || assignedIds.has(uid))
115
+ continue;
116
+ await adapter.create('sys_user_permission_set', { permission_set_id: setId, user_id: uid });
117
+ }
118
+ await load();
119
+ }
120
+ catch (err) {
121
+ const raw = String(err?.body?.error ?? err?.error ?? err?.message ?? '');
122
+ const capMatch = raw.match(/(\d+)\s*of\s*(\d+)\s*seat/i);
123
+ if (/cap reached|seat cap|ai[-_ ]?seat/i.test(raw)) {
124
+ setError(c.seatFull(capMatch ? Number(capMatch[2]) : rows.length));
125
+ }
126
+ else {
127
+ const cleaned = raw.replace(/^\s*\[[^\]]*\]\s*/, '').trim();
128
+ setError(cleaned || c.addFailed);
129
+ }
130
+ }
131
+ finally {
132
+ setBusy(false);
133
+ setPickerOpen(false);
134
+ }
135
+ }, [adapter, setId, assignedIds, load, rows.length, c]);
136
+ const removeUser = React.useCallback(async (grantId) => {
137
+ setError(null);
138
+ try {
139
+ await adapter.delete('sys_user_permission_set', grantId);
140
+ await load();
141
+ }
142
+ catch {
143
+ /* keep the row; a failed delete is non-destructive */
144
+ }
145
+ }, [adapter, load]);
146
+ return (_jsxs("div", { className: "px-4 py-4", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", children: [_jsx(Users, { className: "h-4 w-4 text-muted-foreground" }), _jsx("span", { children: c.title }), !loading && (_jsx("span", { className: "text-xs text-muted-foreground font-normal", children: c.countOf(rows.length) }))] }), _jsxs(Button, { variant: "outline", size: "sm", disabled: busy || !setId, onClick: () => {
147
+ setError(null);
148
+ setPickerOpen(true);
149
+ }, className: "gap-1 h-8 text-xs", children: [busy ? _jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }) : _jsx(Plus, { className: "h-3.5 w-3.5" }), c.add] })] }), error && (_jsxs("div", { className: "mb-3 flex items-start gap-2 rounded-md border border-amber-300/60 bg-amber-50 dark:bg-amber-950/30 px-3 py-2 text-xs text-amber-800 dark:text-amber-200", role: "alert", children: [_jsx(AlertCircle, { className: "h-3.5 w-3.5 mt-0.5 shrink-0" }), _jsx("span", { children: error })] })), loading ? (_jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground py-3", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), c.loading] })) : rows.length === 0 ? (_jsx("div", { className: "text-xs text-muted-foreground italic py-3", children: c.empty })) : (_jsx("ul", { className: "divide-y rounded-md border", children: rows.map((r) => (_jsxs("li", { className: "flex items-center gap-3 px-3 py-2", children: [_jsx("div", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-medium shrink-0", children: (r.name || '?').slice(0, 1).toUpperCase() }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-sm truncate", children: r.name }), r.email && r.email !== r.name && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: r.email }))] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => void removeUser(r.grantId), "aria-label": c.remove, title: c.remove, className: "h-7 w-7 p-0 text-muted-foreground hover:text-destructive shrink-0", children: _jsx(X, { className: "h-4 w-4" }) })] }, r.grantId))) })), setId && (_jsx(RecordPickerDialog, { open: pickerOpen, onOpenChange: (o) => setPickerOpen(o), multiple: true, dataSource: adapter, objectName: "sys_user", title: c.pickTitle, onSelect: () => { }, onSelectRecords: (records) => void addUsers(records) }))] }));
150
+ }
151
+ export default AssignedUsersSection;
@@ -14,5 +14,10 @@
14
14
  * (see framework `http-dispatcher.handlePackages`).
15
15
  */
16
16
  import * as React from 'react';
17
+ export declare function CreatePackageDialog({ open, onOpenChange, onCreated, }: {
18
+ open: boolean;
19
+ onOpenChange: (v: boolean) => void;
20
+ onCreated: (id: string) => void;
21
+ }): React.JSX.Element;
17
22
  export declare function PackagesPage(): React.JSX.Element;
18
23
  export default PackagesPage;
@@ -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 } 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 bases = projectPackages ?? [];
194
+ if (projectPackages !== null && bases.length === 0) {
195
+ setShowCreateBase(true);
196
+ return;
197
+ }
198
+ if (bases.length > 0 && !activePackage) {
199
+ navigate(`./new?package=${encodeURIComponent(bases[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);
@@ -237,12 +254,10 @@ function DefaultMetadataList({ type, appName }) {
237
254
  if (!activePackage)
238
255
  return false;
239
256
  const pkg = row.item?._packageId;
240
- const isLocal = !pkg || pkg === LOCAL_PACKAGE_ID;
241
- // Local/Custom scope surfaces this environment's runtime-authored items
242
- // (untagged / `sys_metadata` provenance); a code package shows its own.
243
- if (activePackage === LOCAL_PACKAGE_ID)
244
- return isLocal;
245
- return !isLocal && pkg === activePackage;
257
+ // Only rows tagged with the active writable base match. Untagged /
258
+ // `sys_metadata`-provenance legacy rows have no scope of their own
259
+ // (ADR-0070 D5 the package-less "Local / Custom" scope is removed).
260
+ return pkg === activePackage;
246
261
  }), [items, activePackage, config]);
247
262
  // User-driven filters (search query + source provenance) on top of scope.
248
263
  const filtered = scopedItems.filter((row) => {
@@ -295,14 +310,14 @@ function DefaultMetadataList({ type, appName }) {
295
310
  },
296
311
  ]
297
312
  : []),
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
313
+ ], 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
314
  ? 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
315
+ : 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
316
  ? tFormat('engine.list.emptyType', locale, { type: typeLabel })
302
317
  : tFormat('engine.list.emptyQuery', locale, { query }) }), _jsx(EmptyDescription, { children: config.emptyStateHint ??
303
318
  (entry?.allowOrgOverride || entry?.allowRuntimeCreate
304
319
  ? 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) => {
320
+ : 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
321
  const pk = config.primaryKey ?? 'name';
307
322
  const name = String(row.item[pk] ?? `(unnamed-${i})`);
308
323
  // ADR-0048 — link to this row's OWNING package so the editor
@@ -28,7 +28,7 @@ import { useRecentItems } from '../../context/RecentItemsProvider';
28
28
  import { useMetadataClient, useMetadataTypes, useGlobalDiagnostics, } from './useMetadata';
29
29
  import { MetadataQuickFind } from './QuickFind';
30
30
  import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
31
- import { buildPackageScopeOptions, LOCAL_PACKAGE_ID } from './package-scope';
31
+ import { buildPackageScopeOptions } from './package-scope';
32
32
  const HIDDEN_TYPES = new Set(['field', 'package']);
33
33
  const DOMAIN_ICONS = {
34
34
  data: Database,
@@ -143,10 +143,6 @@ export function StudioHomePage() {
143
143
  return false;
144
144
  if (!activePackage)
145
145
  return false;
146
- // Local/Custom scope: show every runtime-creatable type so the user can
147
- // start authoring any kind of metadata here, even with zero items yet.
148
- if (activePackage === LOCAL_PACKAGE_ID)
149
- return e.allowOrgOverride || e.allowRuntimeCreate;
150
146
  return (packagesByType[e.type] ?? []).includes(activePackage);
151
147
  }), [activePackage, entries, packagesByType]);
152
148
  const writable = React.useMemo(() => visible.filter((e) => e.allowOrgOverride || e.allowRuntimeCreate), [visible]);
@@ -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
+ }
@@ -181,7 +181,7 @@ const ENGINE_STRINGS_EN = {
181
181
  'engine.list.warnCount': '{count} warning(s):',
182
182
  'engine.list.allSources': 'All sources',
183
183
  'engine.list.allPackages': 'All packages',
184
- 'engine.package.local': 'Local / Custom (this env)',
184
+ 'engine.package.writableRequired': 'Pick or create a writable base (package) first — this item cannot be authored into a read-only code package.',
185
185
  'engine.list.packageFilter': 'Package',
186
186
  'engine.list.source.artifact': 'Artifact',
187
187
  'engine.list.source.runtime': 'Runtime',
@@ -599,6 +599,15 @@ const ENGINE_STRINGS_EN = {
599
599
  'engine.packages.detail.disabled': 'Package disabled.',
600
600
  'engine.packages.detail.enabled': 'Package enabled.',
601
601
  'engine.packages.detail.exported': 'Package exported.',
602
+ 'engine.packages.detail.duplicate': 'Duplicate',
603
+ 'engine.packages.detail.duplicating': 'Duplicating…',
604
+ 'engine.packages.detail.duplicatePrompt': 'New package id for the duplicate (a fresh writable base):',
605
+ 'engine.packages.detail.duplicated': 'Package duplicated into a new base.',
606
+ 'engine.packages.detail.adoptOrphans': 'Adopt loose items',
607
+ 'engine.packages.detail.adopting': 'Adopting…',
608
+ 'engine.packages.detail.adoptConfirm': 'Move all package-less (loose) metadata in this environment INTO "{name}"? This rebinds orphaned items to this base.',
609
+ 'engine.packages.detail.adopted': 'Loose items adopted into this base.',
610
+ 'engine.packages.detail.deleteKeepData': 'Delete the DATA too?\n\nOK = also drop all records (destructive). Cancel = keep records, delete only the structure.',
602
611
  'engine.quickfind.placeholder': "Find metadata types or items… (try 'view', 'account')",
603
612
  'engine.quickfind.empty': 'Type to search across all metadata types.',
604
613
  'engine.quickfind.title': 'Quick Find',
@@ -798,6 +807,7 @@ const ENGINE_STRINGS_EN = {
798
807
  'designer.canvas.askAiGenerate': 'Generate fields with AI',
799
808
  };
800
809
  const ENGINE_STRINGS_ZH = {
810
+ 'engine.package.writableRequired': '请先选择或新建一个可写的基座(package)——只读的代码包中无法新建该项。',
801
811
  'engine.directory.title': '元数据',
802
812
  'engine.directory.description': '平台协议共暴露 {count} 个元数据类型(其中 {writable} 个支持运行时覆盖)。点击任意卡片即可浏览、覆盖或创建实例。',
803
813
  'engine.directory.search': '搜索元数据类型…',
@@ -867,7 +877,6 @@ const ENGINE_STRINGS_ZH = {
867
877
  'engine.list.warnCount': '{count} 个警告:',
868
878
  'engine.list.allSources': '全部来源',
869
879
  'engine.list.allPackages': '全部软件包',
870
- 'engine.package.local': '本地 / 自定义(本环境)',
871
880
  'engine.list.packageFilter': '软件包',
872
881
  'engine.list.source.artifact': '代码包',
873
882
  'engine.list.source.runtime': '运行时',
@@ -1284,6 +1293,15 @@ const ENGINE_STRINGS_ZH = {
1284
1293
  'engine.packages.detail.disabled': '软件包已禁用。',
1285
1294
  'engine.packages.detail.enabled': '软件包已启用。',
1286
1295
  'engine.packages.detail.exported': '软件包已导出。',
1296
+ 'engine.packages.detail.duplicate': '复制',
1297
+ 'engine.packages.detail.duplicating': '复制中…',
1298
+ 'engine.packages.detail.duplicatePrompt': '副本的新软件包 id(一个全新的可写基座):',
1299
+ 'engine.packages.detail.duplicated': '软件包已复制为新基座。',
1300
+ 'engine.packages.detail.adoptOrphans': '收编散落项',
1301
+ 'engine.packages.detail.adopting': '收编中…',
1302
+ 'engine.packages.detail.adoptConfirm': '把本环境中所有无软件包(散落)的元数据移动到 "{name}" 吗?这会把孤儿项重新绑定到此基座。',
1303
+ 'engine.packages.detail.adopted': '散落项已收编进此基座。',
1304
+ 'engine.packages.detail.deleteKeepData': '同时删除数据吗?\n\n确定 = 同时删除所有记录(破坏性)。取消 = 保留记录,仅删除结构。',
1287
1305
  'engine.quickfind.placeholder': '搜索元数据类型或条目…(如:view、account)',
1288
1306
  'engine.quickfind.empty': '输入关键字以搜索所有元数据类型。',
1289
1307
  '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;