@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.
Files changed (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. 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
- const SYSTEM_SCOPES = new Set(['system', 'cloud']);
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) => {