@object-ui/app-shell 6.1.0 → 6.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 +110 -0
- package/README.md +10 -1
- package/dist/console/AppContent.js +24 -2
- package/dist/console/ai/AiChatPage.d.ts +8 -0
- package/dist/console/ai/AiChatPage.js +188 -0
- package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
- package/dist/console/ai/ConversationsSidebar.js +111 -0
- package/dist/console/auth/LoginPage.js +19 -2
- package/dist/console/auth/RegisterPage.js +30 -1
- package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
- package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
- package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
- package/dist/console/marketplace/MarketplacePage.js +55 -18
- package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
- package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
- package/dist/console/marketplace/usePackageL10n.js +110 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
- package/dist/console/organizations/OrganizationsPage.js +24 -3
- package/dist/context/FavoritesProvider.d.ts +40 -2
- package/dist/context/FavoritesProvider.js +201 -20
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChatConversation.d.ts +7 -0
- package/dist/hooks/useChatConversation.js +37 -5
- package/dist/hooks/useConversationList.d.ts +25 -0
- package/dist/hooks/useConversationList.js +131 -0
- package/dist/hooks/useNavPins.d.ts +11 -4
- package/dist/hooks/useNavPins.js +104 -53
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/layout/AppHeader.js +2 -2
- package/dist/layout/AppSidebar.js +20 -1
- package/dist/layout/UnifiedSidebar.js +1 -1
- package/dist/providers/ExpressionProvider.d.ts +11 -1
- package/dist/providers/ExpressionProvider.js +11 -6
- package/dist/services/builtinComponents.d.ts +1 -0
- package/dist/services/builtinComponents.js +166 -0
- package/dist/services/componentRegistry.d.ts +63 -0
- package/dist/services/componentRegistry.js +36 -0
- package/dist/views/ComponentNavView.d.ts +6 -0
- package/dist/views/ComponentNavView.js +26 -0
- package/dist/views/RecordDetailView.js +66 -6
- package/dist/views/RecordFormPage.js +15 -3
- package/dist/views/SearchResultsPage.js +4 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +58 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +140 -0
- package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
- package/dist/views/metadata-admin/DirectoryPage.js +135 -0
- package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
- package/dist/views/metadata-admin/LayeredDiff.js +26 -0
- package/dist/views/metadata-admin/PageShell.d.ts +34 -0
- package/dist/views/metadata-admin/PageShell.js +33 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
- package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
- package/dist/views/metadata-admin/QuickFind.js +152 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +7 -0
- package/dist/views/metadata-admin/ResourceEditPage.js +256 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.js +97 -0
- package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
- package/dist/views/metadata-admin/ResourceListPage.js +144 -0
- package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
- package/dist/views/metadata-admin/ResourceRouter.js +47 -0
- package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
- package/dist/views/metadata-admin/SchemaForm.js +556 -0
- package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
- package/dist/views/metadata-admin/default-schemas.js +207 -0
- package/dist/views/metadata-admin/i18n.d.ts +33 -0
- package/dist/views/metadata-admin/i18n.js +303 -0
- package/dist/views/metadata-admin/index.d.ts +31 -0
- package/dist/views/metadata-admin/index.js +33 -0
- package/dist/views/metadata-admin/predicate.d.ts +31 -0
- package/dist/views/metadata-admin/predicate.js +150 -0
- package/dist/views/metadata-admin/registry.d.ts +125 -0
- package/dist/views/metadata-admin/registry.js +48 -0
- package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
- package/dist/views/metadata-admin/useMetadata.js +96 -0
- package/dist/views/metadata-admin/widgets.d.ts +68 -0
- package/dist/views/metadata-admin/widgets.js +287 -0
- package/package.json +27 -26
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* PermissionMatrixEditor — custom editor for `type=permission` (Phase 3e).
|
|
5
|
+
*
|
|
6
|
+
* Renders the Salesforce-style matrix that lives behind a Permission
|
|
7
|
+
* Set / Profile metadata item:
|
|
8
|
+
*
|
|
9
|
+
* • Top section — object-level CRUD + VAMA (View All / Modify All)
|
|
10
|
+
* + lifecycle (Transfer / Restore / Purge).
|
|
11
|
+
* • Lower section — field-level R/W for the fields of any object
|
|
12
|
+
* selected from the table above.
|
|
13
|
+
*
|
|
14
|
+
* Data model (matches `PermissionSetSchema` in
|
|
15
|
+
* `packages/spec/src/security/permission.zod.ts`):
|
|
16
|
+
*
|
|
17
|
+
* {
|
|
18
|
+
* name: string,
|
|
19
|
+
* label?: string,
|
|
20
|
+
* isProfile?: boolean,
|
|
21
|
+
* objects: { [object_name]: ObjectPermission },
|
|
22
|
+
* fields?: { [`${object_name}.${field_name}`]: FieldPermission },
|
|
23
|
+
* systemPermissions?: string[],
|
|
24
|
+
* tabPermissions?: Record<string, 'visible'|'hidden'|'default_on'|'default_off'>,
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* Wiring: registered from `builtinComponents.tsx` as
|
|
28
|
+
* registerMetadataResource({ type: 'permission', EditPage: PermissionMatrixEditPage })
|
|
29
|
+
*
|
|
30
|
+
* The component reads `/api/v1/meta/object` + `/api/v1/meta/field` to
|
|
31
|
+
* enumerate available objects and their fields. Saves through the
|
|
32
|
+
* standard metadata save flow (overlay-aware, OCC, destructive-change
|
|
33
|
+
* dialog already provided by the generic engine — we go through
|
|
34
|
+
* client.save() directly).
|
|
35
|
+
*/
|
|
36
|
+
import * as React from 'react';
|
|
37
|
+
import { useNavigate } from 'react-router-dom';
|
|
38
|
+
import { Save, Loader2, History as HistoryIcon, AlertTriangle, ChevronDown, ChevronRight, } from 'lucide-react';
|
|
39
|
+
import { Button } from '@object-ui/components';
|
|
40
|
+
import { Badge } from '@object-ui/components';
|
|
41
|
+
import { Input } from '@object-ui/components';
|
|
42
|
+
import { Label } from '@object-ui/components';
|
|
43
|
+
import { Switch } from '@object-ui/components';
|
|
44
|
+
import { Checkbox } from '@object-ui/components';
|
|
45
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@object-ui/components';
|
|
46
|
+
import { PageShell } from './PageShell';
|
|
47
|
+
import { useMetadataClient, useMetadataTypes } from './useMetadata';
|
|
48
|
+
import { resolveResourceConfig } from './registry';
|
|
49
|
+
import { t as translate, detectLocale } from './i18n';
|
|
50
|
+
function getObjectActions(locale) {
|
|
51
|
+
return [
|
|
52
|
+
{ key: 'allowCreate', short: 'C', tip: translate('perm.action.create', locale) },
|
|
53
|
+
{ key: 'allowRead', short: 'R', tip: translate('perm.action.read', locale) },
|
|
54
|
+
{ key: 'allowEdit', short: 'U', tip: translate('perm.action.edit', locale) },
|
|
55
|
+
{ key: 'allowDelete', short: 'D', tip: translate('perm.action.delete', locale) },
|
|
56
|
+
{ key: 'allowTransfer', short: 'Tr', tip: translate('perm.action.transfer', locale) },
|
|
57
|
+
{ key: 'allowRestore', short: 'Re', tip: translate('perm.action.restore', locale) },
|
|
58
|
+
{ key: 'allowPurge', short: 'Pu', tip: translate('perm.action.purge', locale) },
|
|
59
|
+
{ key: 'viewAllRecords', short: 'VA', tip: translate('perm.action.viewAll', locale) },
|
|
60
|
+
{ key: 'modifyAllRecords', short: 'MA', tip: translate('perm.action.modifyAll', locale) },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
/* ────────────────────────────────────────────────────────────────── */
|
|
64
|
+
/* Component */
|
|
65
|
+
/* ────────────────────────────────────────────────────────────────── */
|
|
66
|
+
export function PermissionMatrixEditPage({ type, name }) {
|
|
67
|
+
const navigate = useNavigate();
|
|
68
|
+
const client = useMetadataClient();
|
|
69
|
+
const { entries } = useMetadataTypes(client);
|
|
70
|
+
const entry = entries.find((t) => t.type === type);
|
|
71
|
+
const resolved = resolveResourceConfig(type, entry);
|
|
72
|
+
const writable = !!resolved.allowOrgOverride;
|
|
73
|
+
const locale = React.useMemo(() => detectLocale(), []);
|
|
74
|
+
const t = React.useCallback((k) => translate(k, locale), [locale]);
|
|
75
|
+
const OBJECT_ACTIONS = React.useMemo(() => getObjectActions(locale), [locale]);
|
|
76
|
+
const [draft, setDraft] = React.useState({
|
|
77
|
+
name,
|
|
78
|
+
objects: {},
|
|
79
|
+
fields: {},
|
|
80
|
+
});
|
|
81
|
+
const [objects, setObjects] = React.useState([]);
|
|
82
|
+
const [fieldsByObject, setFieldsByObject] = React.useState({});
|
|
83
|
+
const [expanded, setExpanded] = React.useState(new Set());
|
|
84
|
+
const [loading, setLoading] = React.useState(true);
|
|
85
|
+
const [saving, setSaving] = React.useState(false);
|
|
86
|
+
const [error, setError] = React.useState(null);
|
|
87
|
+
const [destructive, setDestructive] = React.useState(null);
|
|
88
|
+
const [filter, setFilter] = React.useState('');
|
|
89
|
+
const [showOnlyEnabled, setShowOnlyEnabled] = React.useState(false);
|
|
90
|
+
/* ── Load draft + object catalog ───────────────────────────── */
|
|
91
|
+
React.useEffect(() => {
|
|
92
|
+
let cancelled = false;
|
|
93
|
+
setLoading(true);
|
|
94
|
+
(async () => {
|
|
95
|
+
try {
|
|
96
|
+
const [lay, objList] = await Promise.all([
|
|
97
|
+
client.layered(type, name).catch(() => null),
|
|
98
|
+
client.list('object').catch(() => []),
|
|
99
|
+
]);
|
|
100
|
+
if (cancelled)
|
|
101
|
+
return;
|
|
102
|
+
const effective = (lay?.effective ??
|
|
103
|
+
lay?.code ?? { name, objects: {} });
|
|
104
|
+
setDraft({
|
|
105
|
+
...effective,
|
|
106
|
+
name: String(effective?.name ?? name),
|
|
107
|
+
objects: effective?.objects ?? {},
|
|
108
|
+
fields: effective?.fields ?? {},
|
|
109
|
+
});
|
|
110
|
+
const list = (objList ?? [])
|
|
111
|
+
.map((row) => {
|
|
112
|
+
const item = row?.item ?? row;
|
|
113
|
+
return { name: String(item?.name ?? ''), label: item?.label };
|
|
114
|
+
})
|
|
115
|
+
.filter((o) => !!o.name)
|
|
116
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
117
|
+
setObjects(list);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
setError(err?.message ?? String(err));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (!cancelled)
|
|
124
|
+
setLoading(false);
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
127
|
+
return () => {
|
|
128
|
+
cancelled = true;
|
|
129
|
+
};
|
|
130
|
+
}, [client, type, name]);
|
|
131
|
+
/* ── Lazy-load fields when an object is expanded ─────────── */
|
|
132
|
+
async function ensureFields(objectName) {
|
|
133
|
+
if (fieldsByObject[objectName])
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
// Fields are stored as `${object}__${field}` keys under the
|
|
137
|
+
// `field` metadata type. We resolve by listing then filtering
|
|
138
|
+
// on the `object` attribute of the item.
|
|
139
|
+
const items = await client.list('field');
|
|
140
|
+
const list = (items ?? [])
|
|
141
|
+
.map((row) => row?.item ?? row)
|
|
142
|
+
.filter((f) => String(f?.object ?? '') === objectName)
|
|
143
|
+
.map((f) => ({ name: String(f?.name ?? ''), label: f?.label }))
|
|
144
|
+
.filter((f) => !!f.name)
|
|
145
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
146
|
+
setFieldsByObject((prev) => ({ ...prev, [objectName]: list }));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
setFieldsByObject((prev) => ({ ...prev, [objectName]: [] }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function toggleExpand(objectName) {
|
|
153
|
+
setExpanded((prev) => {
|
|
154
|
+
const next = new Set(prev);
|
|
155
|
+
if (next.has(objectName))
|
|
156
|
+
next.delete(objectName);
|
|
157
|
+
else {
|
|
158
|
+
next.add(objectName);
|
|
159
|
+
ensureFields(objectName);
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/* ── Mutators ───────────────────────────────────────────── */
|
|
165
|
+
function updateObjectPerm(objectName, action, value) {
|
|
166
|
+
setDraft((prev) => {
|
|
167
|
+
const cur = prev.objects[objectName] ?? {};
|
|
168
|
+
const nextObj = { ...cur, [action]: value };
|
|
169
|
+
// Cascade: viewAllRecords implies allowRead.
|
|
170
|
+
if (action === 'viewAllRecords' && value)
|
|
171
|
+
nextObj.allowRead = true;
|
|
172
|
+
if (action === 'modifyAllRecords' && value) {
|
|
173
|
+
nextObj.allowEdit = true;
|
|
174
|
+
nextObj.allowRead = true;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...prev,
|
|
178
|
+
objects: { ...prev.objects, [objectName]: nextObj },
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function bulkSetObject(objectName, action) {
|
|
183
|
+
setDraft((prev) => {
|
|
184
|
+
const next = action === 'none'
|
|
185
|
+
? {}
|
|
186
|
+
: action === 'all'
|
|
187
|
+
? Object.fromEntries(OBJECT_ACTIONS.map((a) => [a.key, true]))
|
|
188
|
+
: action === 'crud'
|
|
189
|
+
? { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true }
|
|
190
|
+
: { allowRead: true };
|
|
191
|
+
return {
|
|
192
|
+
...prev,
|
|
193
|
+
objects: { ...prev.objects, [objectName]: next },
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function updateFieldPerm(objectName, fieldName, action, value) {
|
|
198
|
+
const key = `${objectName}.${fieldName}`;
|
|
199
|
+
setDraft((prev) => {
|
|
200
|
+
const fields = { ...(prev.fields ?? {}) };
|
|
201
|
+
const cur = fields[key] ?? { readable: true, editable: false };
|
|
202
|
+
const next = { ...cur, [action]: value };
|
|
203
|
+
// Cascade: !readable implies !editable.
|
|
204
|
+
if (action === 'readable' && !value)
|
|
205
|
+
next.editable = false;
|
|
206
|
+
// Cascade: editable implies readable.
|
|
207
|
+
if (action === 'editable' && value)
|
|
208
|
+
next.readable = true;
|
|
209
|
+
fields[key] = next;
|
|
210
|
+
return { ...prev, fields };
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/* ── Save ────────────────────────────────────────────────── */
|
|
214
|
+
async function doSave(force, pending) {
|
|
215
|
+
const payload = pending ?? draft;
|
|
216
|
+
setSaving(true);
|
|
217
|
+
setError(null);
|
|
218
|
+
try {
|
|
219
|
+
await client.save(type, payload.name, payload, {
|
|
220
|
+
force,
|
|
221
|
+
});
|
|
222
|
+
const lay = await client.layered(type, payload.name);
|
|
223
|
+
setDraft((lay.effective ?? payload));
|
|
224
|
+
setDestructive(null);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (err?.status === 409 && err?.code === 'destructive_change') {
|
|
228
|
+
const issues = err?.body?.issues ?? [];
|
|
229
|
+
setDestructive({ issues: Array.isArray(issues) ? issues : [], pending: payload });
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
setError(err?.message ?? String(err));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
setSaving(false);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/* ── Render helpers ──────────────────────────────────────── */
|
|
240
|
+
const filteredObjects = React.useMemo(() => {
|
|
241
|
+
const q = filter.trim().toLowerCase();
|
|
242
|
+
return objects.filter((o) => {
|
|
243
|
+
if (showOnlyEnabled) {
|
|
244
|
+
const perm = draft.objects[o.name];
|
|
245
|
+
if (!perm || !Object.values(perm).some(Boolean))
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (!q)
|
|
249
|
+
return true;
|
|
250
|
+
return (o.name.toLowerCase().includes(q) ||
|
|
251
|
+
(o.label ?? '').toLowerCase().includes(q));
|
|
252
|
+
});
|
|
253
|
+
}, [objects, filter, showOnlyEnabled, draft.objects]);
|
|
254
|
+
const stats = [
|
|
255
|
+
{
|
|
256
|
+
label: t('perm.stat.objectsGranted'),
|
|
257
|
+
value: Object.values(draft.objects).filter((p) => Object.values(p).some(Boolean)).length,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
label: t('perm.stat.fieldOverrides'),
|
|
261
|
+
value: Object.keys(draft.fields ?? {}).length,
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
if (loading) {
|
|
265
|
+
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
|
+
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
|
+
}
|
|
269
|
+
function PermissionTable({ objects, draft, expanded, fieldsByObject, writable, objectActions, t, onToggleExpand, onObjectPerm, onFieldPerm, onBulkSet, }) {
|
|
270
|
+
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) => {
|
|
271
|
+
const perm = draft.objects[o.name] ?? {};
|
|
272
|
+
const open = expanded.has(o.name);
|
|
273
|
+
return (_jsxs(React.Fragment, { children: [_jsxs("tr", { className: "border-b hover:bg-muted/30", children: [_jsx("td", { className: "px-2 py-1.5 align-middle", children: _jsxs("button", { type: "button", onClick: () => onToggleExpand(o.name), className: "inline-flex items-center gap-1.5 hover:text-foreground", children: [open ? (_jsx(ChevronDown, { className: "h-3.5 w-3.5" })) : (_jsx(ChevronRight, { className: "h-3.5 w-3.5" })), _jsx("span", { className: "font-medium", children: o.label ?? o.name }), o.label && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["(", o.name, ")"] }))] }) }), objectActions.map((a) => (_jsx("td", { className: "text-center px-2 py-1.5", children: _jsx(Checkbox, { checked: !!perm[a.key], disabled: !writable, onCheckedChange: (v) => onObjectPerm(o.name, a.key, !!v), "aria-label": `${o.name} ${a.tip}` }) }, a.key))), _jsxs("td", { className: "px-2 py-1.5 text-right space-x-1", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "h-6 px-1.5 text-xs", disabled: !writable, onClick: () => onBulkSet(o.name, 'read'), children: t('perm.bulk.read') }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 px-1.5 text-xs", disabled: !writable, onClick: () => onBulkSet(o.name, 'crud'), children: t('perm.bulk.crud') }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 px-1.5 text-xs", disabled: !writable, onClick: () => onBulkSet(o.name, 'all'), children: t('perm.bulk.all') }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 px-1.5 text-xs", disabled: !writable, onClick: () => onBulkSet(o.name, 'none'), children: t('perm.bulk.none') })] })] }), open && (_jsx("tr", { className: "bg-muted/10", children: _jsx("td", { colSpan: objectActions.length + 2, className: "px-12 py-3", children: _jsx(FieldsSubTable, { objectName: o.name, fields: fieldsByObject[o.name], fieldsState: draft.fields ?? {}, writable: writable, t: t, onFieldPerm: onFieldPerm }) }) }))] }, o.name));
|
|
274
|
+
})] })] }));
|
|
275
|
+
}
|
|
276
|
+
function FieldsSubTable({ objectName, fields, fieldsState, writable, t, onFieldPerm, }) {
|
|
277
|
+
if (!fields) {
|
|
278
|
+
return (_jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3 w-3 animate-spin" }), " ", t('perm.field.loading')] }));
|
|
279
|
+
}
|
|
280
|
+
if (fields.length === 0) {
|
|
281
|
+
return _jsx("div", { className: "text-xs text-muted-foreground", children: t('perm.field.empty') });
|
|
282
|
+
}
|
|
283
|
+
return (_jsxs("table", { className: "w-full text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-muted-foreground", children: [_jsx("th", { className: "text-left py-1 font-medium", children: t('perm.field.col.name') }), _jsx("th", { className: "px-2 py-1 font-medium w-16 text-center", title: t('perm.field.read'), children: t('perm.field.read') }), _jsx("th", { className: "px-2 py-1 font-medium w-16 text-center", title: t('perm.field.edit'), children: t('perm.field.edit') })] }) }), _jsx("tbody", { children: fields.map((f) => {
|
|
284
|
+
const key = `${objectName}.${f.name}`;
|
|
285
|
+
const cur = fieldsState[key] ?? { readable: true, editable: false };
|
|
286
|
+
return (_jsxs("tr", { className: "border-t border-muted", children: [_jsxs("td", { className: "py-1", children: [f.label ?? f.name, f.label && (_jsxs("span", { className: "ml-1 text-muted-foreground", children: ["(", f.name, ")"] }))] }), _jsx("td", { className: "px-2 py-1 text-center", children: _jsx(Checkbox, { checked: !!cur.readable, disabled: !writable, onCheckedChange: (v) => onFieldPerm(objectName, f.name, 'readable', !!v), "aria-label": `${objectName}.${f.name} readable` }) }), _jsx("td", { className: "px-2 py-1 text-center", children: _jsx(Checkbox, { checked: !!cur.editable, disabled: !writable, onCheckedChange: (v) => onFieldPerm(objectName, f.name, 'editable', !!v), "aria-label": `${objectName}.${f.name} editable` }) })] }, f.name));
|
|
287
|
+
}) })] }));
|
|
288
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* MetadataQuickFind — Cmd+K palette that searches across all
|
|
5
|
+
* metadata types (Phase 3c).
|
|
6
|
+
*
|
|
7
|
+
* Behaviour:
|
|
8
|
+
* • Mounted as a side-effect via `<MetadataQuickFindMount />`.
|
|
9
|
+
* • Listens for Cmd+K / Ctrl+K globally; opens a dialog with a
|
|
10
|
+
* search input.
|
|
11
|
+
* • Free-text search hits two pools:
|
|
12
|
+
* (a) Metadata types from `/meta/types` (jump to directory entry).
|
|
13
|
+
* (b) Items of any type — lazy-fetched the first time the palette
|
|
14
|
+
* opens, cached for the session.
|
|
15
|
+
* • Selecting a result navigates to the appropriate route.
|
|
16
|
+
*
|
|
17
|
+
* Trade-offs: For the MVP we eagerly fetch the item index on first
|
|
18
|
+
* open (one `/meta/:type` per writable type). For most workspaces
|
|
19
|
+
* that's a few hundred items total — well within a fast modal load.
|
|
20
|
+
* If usage grows we'll switch to server-side search.
|
|
21
|
+
*/
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
import { useNavigate } from 'react-router-dom';
|
|
24
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@object-ui/components';
|
|
25
|
+
import { Input } from '@object-ui/components';
|
|
26
|
+
import { Badge } from '@object-ui/components';
|
|
27
|
+
import { Kbd } from '@object-ui/components';
|
|
28
|
+
import { Search, Loader2 } from 'lucide-react';
|
|
29
|
+
import { useMetadataClient, useMetadataTypes, } from './useMetadata';
|
|
30
|
+
import { detectLocale, t as tr } from './i18n';
|
|
31
|
+
export function MetadataQuickFind({ appSlug } = {}) {
|
|
32
|
+
const navigate = useNavigate();
|
|
33
|
+
const client = useMetadataClient();
|
|
34
|
+
const { entries: typeEntries } = useMetadataTypes(client);
|
|
35
|
+
const [open, setOpen] = React.useState(false);
|
|
36
|
+
const [query, setQuery] = React.useState('');
|
|
37
|
+
const [items, setItems] = React.useState(null);
|
|
38
|
+
const [itemsLoading, setItemsLoading] = React.useState(false);
|
|
39
|
+
const [activeIdx, setActiveIdx] = React.useState(0);
|
|
40
|
+
const locale = React.useMemo(() => detectLocale(), []);
|
|
41
|
+
// Global Cmd+Shift+M listener (avoids clashing with the existing
|
|
42
|
+
// CommandPalette which owns Cmd+K).
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
function onKey(e) {
|
|
45
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'm') {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setOpen((v) => !v);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
window.addEventListener('keydown', onKey);
|
|
51
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
52
|
+
}, []);
|
|
53
|
+
// Lazy-fetch item index when palette first opens.
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (!open || items != null || itemsLoading || typeEntries.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
setItemsLoading(true);
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
(async () => {
|
|
60
|
+
try {
|
|
61
|
+
// Index *all* types (read-only ones are fine — we just won't
|
|
62
|
+
// offer create/edit for them; users may still want to find code
|
|
63
|
+
// artifacts to inspect).
|
|
64
|
+
const all = [];
|
|
65
|
+
await Promise.all(typeEntries.map(async (t) => {
|
|
66
|
+
try {
|
|
67
|
+
const list = await client.list(t.type);
|
|
68
|
+
for (const raw of list ?? []) {
|
|
69
|
+
const item = (raw && typeof raw === 'object' && 'item' in raw ? raw.item : raw) ?? {};
|
|
70
|
+
const name = item?.name;
|
|
71
|
+
if (!name)
|
|
72
|
+
continue;
|
|
73
|
+
all.push({
|
|
74
|
+
kind: 'item',
|
|
75
|
+
type: t.type,
|
|
76
|
+
name: String(name),
|
|
77
|
+
label: item?.label,
|
|
78
|
+
description: item?.description,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* skip types that error */
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
if (!cancelled)
|
|
87
|
+
setItems(all);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (!cancelled)
|
|
91
|
+
setItemsLoading(false);
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
}, [open, items, itemsLoading, typeEntries, client]);
|
|
98
|
+
// Compose results: typed matches first, then item matches.
|
|
99
|
+
const results = React.useMemo(() => {
|
|
100
|
+
const q = query.trim().toLowerCase();
|
|
101
|
+
if (!q) {
|
|
102
|
+
// Empty query → show the type directory only.
|
|
103
|
+
return typeEntries.slice(0, 20).map((entry) => ({ kind: 'type', entry }));
|
|
104
|
+
}
|
|
105
|
+
const typeHits = typeEntries
|
|
106
|
+
.filter((e) => e.type.toLowerCase().includes(q) ||
|
|
107
|
+
(e.label ?? '').toLowerCase().includes(q) ||
|
|
108
|
+
(e.description ?? '').toLowerCase().includes(q))
|
|
109
|
+
.slice(0, 8)
|
|
110
|
+
.map((entry) => ({ kind: 'type', entry }));
|
|
111
|
+
const itemHits = (items ?? [])
|
|
112
|
+
.filter((i) => i.name.toLowerCase().includes(q) ||
|
|
113
|
+
(i.label ?? '').toLowerCase().includes(q) ||
|
|
114
|
+
(i.description ?? '').toLowerCase().includes(q) ||
|
|
115
|
+
i.type.toLowerCase().includes(q))
|
|
116
|
+
.slice(0, 40);
|
|
117
|
+
return [...typeHits, ...itemHits];
|
|
118
|
+
}, [query, typeEntries, items]);
|
|
119
|
+
React.useEffect(() => {
|
|
120
|
+
setActiveIdx(0);
|
|
121
|
+
}, [query, open]);
|
|
122
|
+
function navigateTo(r) {
|
|
123
|
+
setOpen(false);
|
|
124
|
+
const base = appSlug ? `/apps/${appSlug}` : '..';
|
|
125
|
+
if (r.kind === 'type') {
|
|
126
|
+
navigate(`${base}/component/metadata/resource?type=${encodeURIComponent(r.entry.type)}`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
navigate(`${base}/component/metadata/resource/${encodeURIComponent(r.name)}?type=${encodeURIComponent(r.type)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function onKeyDown(e) {
|
|
133
|
+
if (e.key === 'ArrowDown') {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
setActiveIdx((i) => Math.min(i + 1, results.length - 1));
|
|
136
|
+
}
|
|
137
|
+
else if (e.key === 'ArrowUp') {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
setActiveIdx((i) => Math.max(i - 1, 0));
|
|
140
|
+
}
|
|
141
|
+
else if (e.key === 'Enter') {
|
|
142
|
+
if (results[activeIdx]) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
navigateTo(results[activeIdx]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return (_jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-2xl p-0 overflow-hidden", children: [_jsx(DialogHeader, { className: "sr-only", children: _jsx(DialogTitle, { children: tr('engine.quickfind.title', locale) }) }), _jsxs("div", { className: "border-b px-3 py-2 flex items-center gap-2", children: [_jsx(Search, { className: "h-4 w-4 text-muted-foreground" }), _jsx(Input, { autoFocus: true, placeholder: tr('engine.quickfind.placeholder', locale), value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: onKeyDown, className: "border-0 shadow-none focus-visible:ring-0 px-0" }), _jsx(Kbd, { children: "esc" })] }), _jsxs("div", { className: "max-h-[440px] overflow-auto", children: [itemsLoading && (_jsxs("div", { className: "px-3 py-4 text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), tr('engine.quickfind.indexing', locale), " ", typeEntries.length, "\u2026"] })), !itemsLoading && results.length === 0 && (_jsx("div", { className: "px-3 py-6 text-xs text-muted-foreground text-center", children: tr('engine.quickfind.noMatches', locale) })), _jsx("ul", { role: "listbox", children: results.map((r, i) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => navigateTo(r), onMouseEnter: () => setActiveIdx(i), className: 'w-full text-left px-3 py-2 flex items-center gap-3 hover:bg-accent ' +
|
|
149
|
+
(i === activeIdx ? 'bg-accent' : ''), children: [_jsx(Badge, { variant: "outline", className: "text-[10px] font-mono shrink-0", children: r.kind === 'type' ? 'type' : r.type }), _jsx("div", { className: "min-w-0 flex-1", children: r.kind === 'type' ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "text-sm font-medium truncate", children: r.entry.label ?? r.entry.type }), r.entry.description && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: r.entry.description }))] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "text-sm font-mono truncate", children: [r.name, r.label && (_jsx("span", { className: "text-muted-foreground font-sans ml-2", children: r.label }))] }), r.description && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: r.description }))] })) })] }) }, r.kind === 'type'
|
|
150
|
+
? `t:${r.entry.type}`
|
|
151
|
+
: `i:${r.type}:${r.name}`))) })] }), _jsxs("div", { className: "border-t px-3 py-1.5 flex items-center gap-3 text-[10px] text-muted-foreground", children: [_jsxs("span", { children: [_jsx(Kbd, { children: "\u2191" }), " ", _jsx(Kbd, { children: "\u2193" }), " navigate"] }), _jsxs("span", { children: [_jsx(Kbd, { children: "\u21B5" }), " open"] }), _jsxs("span", { className: "ml-auto", children: [_jsx(Kbd, { children: "\u2318" }), _jsx(Kbd, { children: "\u21E7" }), _jsx(Kbd, { children: "M" }), " toggle"] })] })] }) }));
|
|
152
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface MetadataResourceEditPageProps {
|
|
2
|
+
type: string;
|
|
3
|
+
name: string;
|
|
4
|
+
/** When true, this is the Create flow (skip initial fetch). */
|
|
5
|
+
createMode?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function MetadataResourceEditPage({ type, name, createMode, }: MetadataResourceEditPageProps): import("react/jsx-runtime").JSX.Element;
|