@object-ui/app-shell 7.0.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +560 -0
- package/dist/console/AppContent.js +23 -17
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +47 -16
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +42 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -2
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +41 -10
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +58 -5
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
- package/dist/views/metadata-admin/ResourceListPage.js +28 -19
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- 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 +108 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
- 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 +81 -4
- 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/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
- 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/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +41 -0
- package/dist/views/metadata-admin/package-scope.js +59 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- 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/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
- 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/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -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;
|
|
@@ -25,6 +25,7 @@ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
|
25
25
|
import { useMetadataClient, useMetadataTypes, useGlobalDiagnostics, } from './useMetadata';
|
|
26
26
|
import { MetadataQuickFind } from './QuickFind';
|
|
27
27
|
import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
|
|
28
|
+
import { buildPackageScopeOptions } from './package-scope';
|
|
28
29
|
const DOMAIN_ICONS = {
|
|
29
30
|
data: Database,
|
|
30
31
|
ui: Layers,
|
|
@@ -79,20 +80,7 @@ export function MetadataDirectoryPage() {
|
|
|
79
80
|
const list = await client.list('package');
|
|
80
81
|
if (cancelled)
|
|
81
82
|
return;
|
|
82
|
-
|
|
83
|
-
const rows = (list ?? [])
|
|
84
|
-
.map((raw) => {
|
|
85
|
-
const item = raw && typeof raw === 'object' && 'item' in raw ? raw.item : raw;
|
|
86
|
-
const m = (item?.manifest ?? item ?? {});
|
|
87
|
-
return {
|
|
88
|
-
id: m.id,
|
|
89
|
-
scope: m.scope,
|
|
90
|
-
name: m.name || m.id,
|
|
91
|
-
};
|
|
92
|
-
})
|
|
93
|
-
.filter((p) => p.id && !SYSTEM_SCOPES.has(p.scope));
|
|
94
|
-
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
95
|
-
setProjectPackages(rows.map((p) => ({ id: p.id, name: p.name })));
|
|
83
|
+
setProjectPackages(buildPackageScopeOptions(list));
|
|
96
84
|
}
|
|
97
85
|
catch {
|
|
98
86
|
if (!cancelled)
|
|
@@ -32,6 +32,8 @@ export interface JsonSourceEditorProps {
|
|
|
32
32
|
issues?: JsonIssue[];
|
|
33
33
|
/** Pixel or CSS-length height. Defaults to `60vh`. */
|
|
34
34
|
height?: string | number;
|
|
35
|
+
/** Grace period (ms) before the textarea fallback engages. Test-tunable. */
|
|
36
|
+
fallbackDelayMs?: number;
|
|
35
37
|
}
|
|
36
|
-
export declare function JsonSourceEditor({ value, onChange, readOnly, issues, height, }: JsonSourceEditorProps): React.JSX.Element;
|
|
38
|
+
export declare function JsonSourceEditor({ value, onChange, readOnly, issues, height, fallbackDelayMs, }: JsonSourceEditorProps): React.JSX.Element;
|
|
37
39
|
export default JsonSourceEditor;
|
|
@@ -45,7 +45,7 @@ function splitPath(p) {
|
|
|
45
45
|
return Number.isInteger(n) && String(n) === seg ? n : seg;
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '60vh', }) {
|
|
48
|
+
export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '60vh', fallbackDelayMs = 4000, }) {
|
|
49
49
|
const locale = React.useMemo(() => detectLocale(), []);
|
|
50
50
|
const [text, setText] = React.useState(() => stringify(value));
|
|
51
51
|
const [parseError, setParseError] = React.useState(null);
|
|
@@ -54,6 +54,24 @@ export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '
|
|
|
54
54
|
// when either the source text or the issues prop changes.
|
|
55
55
|
const editorRef = React.useRef(null);
|
|
56
56
|
const monacoRef = React.useRef(null);
|
|
57
|
+
// Monaco's core is fetched lazily and, by default, from a public CDN, and it
|
|
58
|
+
// also spins up web workers. When any of that is blocked — offline /
|
|
59
|
+
// air-gapped / CSP-restricted installs — the editor mounts an empty shell
|
|
60
|
+
// with no error and the Source tab looks blank. Detect "nothing actually
|
|
61
|
+
// painted" via the rendered `.view-line` rows and fall back to a plain
|
|
62
|
+
// textarea so the source is always readable and editable.
|
|
63
|
+
const containerRef = React.useRef(null);
|
|
64
|
+
const [monacoUnavailable, setMonacoUnavailable] = React.useState(false);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
if (monacoUnavailable)
|
|
67
|
+
return;
|
|
68
|
+
const id = setTimeout(() => {
|
|
69
|
+
const el = containerRef.current;
|
|
70
|
+
if (!el || !el.querySelector('.view-line'))
|
|
71
|
+
setMonacoUnavailable(true);
|
|
72
|
+
}, fallbackDelayMs);
|
|
73
|
+
return () => clearTimeout(id);
|
|
74
|
+
}, [monacoUnavailable, fallbackDelayMs]);
|
|
57
75
|
// Match against the dark class our app-shell toggles on <html>; pick
|
|
58
76
|
// a Monaco theme that doesn't fight the rest of the chrome.
|
|
59
77
|
const [theme, setTheme] = React.useState(() => {
|
|
@@ -161,7 +179,7 @@ export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '
|
|
|
161
179
|
// Defer one tick so the model has settled before the first paint.
|
|
162
180
|
setTimeout(applyMarkers, 0);
|
|
163
181
|
};
|
|
164
|
-
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { className: "border rounded overflow-hidden bg-background", style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "w-full h-full" }), children: _jsx(LazyMonaco, { value: text, language: "json", theme: theme, onChange: handleChange, onMount: handleMount, options: {
|
|
182
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("div", { ref: containerRef, "data-testid": "source-editor", className: "border rounded overflow-hidden bg-background", style: { height: typeof height === 'number' ? `${height}px` : height }, children: monacoUnavailable ? (_jsx("textarea", { value: text, onChange: (e) => handleChange(e.target.value), readOnly: readOnly, spellCheck: false, "aria-label": "JSON source", className: "w-full h-full resize-none bg-background p-3 font-mono text-xs leading-relaxed outline-none" })) : (_jsx(React.Suspense, { fallback: _jsx(Skeleton, { className: "w-full h-full" }), children: _jsx(LazyMonaco, { value: text, language: "json", theme: theme, onChange: handleChange, onMount: handleMount, options: {
|
|
165
183
|
readOnly,
|
|
166
184
|
minimap: { enabled: false },
|
|
167
185
|
fontSize: 12,
|
|
@@ -173,6 +191,6 @@ export function JsonSourceEditor({ value, onChange, readOnly, issues, height = '
|
|
|
173
191
|
tabSize: 2,
|
|
174
192
|
renderLineHighlight: 'line',
|
|
175
193
|
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
176
|
-
} }) }) }), parseError && (_jsxs("div", { className: "text-xs text-destructive flex items-start gap-1.5", children: [_jsx("span", { "aria-hidden": true, children: "\u26A0" }), _jsx("span", { children: parseError })] }))] }));
|
|
194
|
+
} }) })) }), parseError && (_jsxs("div", { className: "text-xs text-destructive flex items-start gap-1.5", children: [_jsx("span", { "aria-hidden": true, children: "\u26A0" }), _jsx("span", { children: parseError })] }))] }));
|
|
177
195
|
}
|
|
178
196
|
export default JsonSourceEditor;
|
|
@@ -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('');
|
|
@@ -104,6 +104,14 @@ function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
|
104
104
|
}),
|
|
105
105
|
});
|
|
106
106
|
onCreated(id.trim());
|
|
107
|
+
// Let context selectors (e.g. the sidebar package switcher) pick up the
|
|
108
|
+
// new package without a full page reload.
|
|
109
|
+
try {
|
|
110
|
+
window.dispatchEvent(new CustomEvent('objectui:packages-changed'));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* non-DOM env */
|
|
114
|
+
}
|
|
107
115
|
onOpenChange(false);
|
|
108
116
|
}
|
|
109
117
|
catch (e) {
|
|
@@ -113,7 +121,7 @@ function CreatePackageDialog({ open, onOpenChange, onCreated, }) {
|
|
|
113
121
|
setBusy(false);
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('engine.packages.create.title', locale) }), _jsx(DialogDescription, { children: tFormat('engine.packages.create.description', locale, { example: 'com.acme.crm' }) })] }), _jsxs("div", { className: "space-y-4 py-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-id", children: t('engine.packages.create.id', locale) }), _jsx(Input, { id: "pkg-id", placeholder: "com.acme.crm", value: id, onChange: (e) => setId(e.target.value), "aria-invalid": !!id && !idValid }), !!id && !idValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.idInvalid', locale) }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-name", children: t('engine.packages.create.name', locale) }), _jsx(Input, { id: "pkg-name", placeholder: "Acme CRM", value: name, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-version", children: t('engine.packages.create.version', locale) }), _jsx(Input, { id: "pkg-version", placeholder: "0.1.0", value: version, onChange: (e) => setVersion(e.target.value), "aria-invalid": !!version && !versionValid }), !!version && !versionValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.versionInvalid', locale) }))] }), error && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: error })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: t('engine.cancel', locale) }), _jsx(Button, { onClick: submit, disabled: !canSubmit, children: busy ? t('engine.packages.create.creating', locale) : t('engine.packages.create.submit', locale) })] })] }) }));
|
|
124
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('engine.packages.create.title', locale) }), _jsx(DialogDescription, { children: tFormat('engine.packages.create.description', locale, { example: 'com.acme.crm' }) })] }), _jsxs("div", { className: "space-y-4 py-2", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-id", children: t('engine.packages.create.id', locale) }), _jsx(Input, { id: "pkg-id", "data-testid": "package-id-input", placeholder: "com.acme.crm", value: id, onChange: (e) => setId(e.target.value), "aria-invalid": !!id && !idValid }), !!id && !idValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.idInvalid', locale) }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-name", children: t('engine.packages.create.name', locale) }), _jsx(Input, { id: "pkg-name", "data-testid": "package-name-input", placeholder: "Acme CRM", value: name, onChange: (e) => setName(e.target.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "pkg-version", children: t('engine.packages.create.version', locale) }), _jsx(Input, { id: "pkg-version", placeholder: "0.1.0", value: version, onChange: (e) => setVersion(e.target.value), "aria-invalid": !!version && !versionValid }), !!version && !versionValid && (_jsx("p", { className: "text-xs text-destructive", children: t('engine.packages.create.versionInvalid', locale) }))] }), error && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-2 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: error })] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: busy, children: t('engine.cancel', locale) }), _jsx(Button, { onClick: submit, disabled: !canSubmit, children: busy ? t('engine.packages.create.creating', locale) : t('engine.packages.create.submit', locale) })] })] }) }));
|
|
117
125
|
}
|
|
118
126
|
/* -------------------------------------------------------------------------- */
|
|
119
127
|
/* Detail sheet — manifest + lifecycle actions */
|
|
@@ -234,10 +242,13 @@ function PackageDetailSheet({ pkg, appBase, open, onOpenChange, onChanged, }) {
|
|
|
234
242
|
const ok = window.confirm(tFormat('engine.packages.detail.deleteConfirm', locale, { name: pkg?.manifest.name || id }));
|
|
235
243
|
if (!ok)
|
|
236
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';
|
|
237
248
|
setBusy('delete');
|
|
238
249
|
setMsg(null);
|
|
239
250
|
try {
|
|
240
|
-
await apiJson(`${API}/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
251
|
+
await apiJson(`${API}/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' });
|
|
241
252
|
onChanged();
|
|
242
253
|
onOpenChange(false);
|
|
243
254
|
}
|
|
@@ -248,6 +259,48 @@ function PackageDetailSheet({ pkg, appBase, open, onOpenChange, onChanged, }) {
|
|
|
248
259
|
setBusy(null);
|
|
249
260
|
}
|
|
250
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
|
+
};
|
|
251
304
|
const toggleEnable = () => run('toggle', () => apiJson(`${API}/${encodeURIComponent(id)}/${enabled ? 'disable' : 'enable'}`, {
|
|
252
305
|
method: 'PATCH',
|
|
253
306
|
}), enabled ? t('engine.packages.detail.disabled', locale) : t('engine.packages.detail.enabled', locale));
|
|
@@ -269,7 +322,7 @@ function PackageDetailSheet({ pkg, appBase, open, onOpenChange, onChanged, }) {
|
|
|
269
322
|
? t('engine.packages.detail.publishing', locale)
|
|
270
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'
|
|
271
324
|
? t('engine.packages.detail.discarding', locale)
|
|
272
|
-
: 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'
|
|
273
326
|
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400'
|
|
274
327
|
: 'border-destructive/40 bg-destructive/10 text-destructive'}`, children: msg.text }))] }) }));
|
|
275
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) => {
|