@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.
- package/CHANGELOG.md +320 -0
- package/dist/components/ManagedByBadge.js +1 -1
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +64 -14
- package/dist/console/ai/BuildDebugDrawer.d.ts +20 -0
- package/dist/console/ai/BuildDebugDrawer.js +75 -0
- package/dist/console/ai/buildDebugApi.d.ts +94 -0
- package/dist/console/ai/buildDebugApi.js +16 -0
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +32 -4
- package/dist/console/organizations/manage/OrganizationLayout.js +1 -1
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +30 -5
- package/dist/layout/ConsoleFloatingChatbot.js +22 -4
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +0 -19
- package/dist/layout/WorkspaceSwitcher.d.ts +14 -0
- package/dist/layout/WorkspaceSwitcher.js +76 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/managedByEmptyState.d.ts +1 -1
- package/dist/utils/managedByEmptyState.js +20 -2
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +27 -13
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +25 -10
- package/dist/views/metadata-admin/StudioHomePage.js +1 -5
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +9 -19
- package/dist/views/metadata-admin/package-scope.js +11 -25
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -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
|
|
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
|
|
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
|
-
|
|
241
|
-
//
|
|
242
|
-
// (
|
|
243
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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.
|
|
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;
|