@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,140 @@
|
|
|
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
|
+
* DesignerEditorWrapper — Phase 3d.
|
|
5
|
+
*
|
|
6
|
+
* Generic "load metadata → hand to a controlled designer component →
|
|
7
|
+
* save on commit" wrapper. Lets us plug existing bespoke designers
|
|
8
|
+
* (ObjectViewConfigurator, DashboardEditor, PageDesigner, …) into the
|
|
9
|
+
* unified Setup-app shell without rewriting them.
|
|
10
|
+
*
|
|
11
|
+
* Each designer takes a different prop shape, so we accept a
|
|
12
|
+
* `renderDesigner` callback that gets `(value, onChange, readOnly)`
|
|
13
|
+
* and returns whatever the designer needs.
|
|
14
|
+
*
|
|
15
|
+
* Wiring is dead simple — see `builtinComponents.tsx`:
|
|
16
|
+
*
|
|
17
|
+
* registerMetadataResource({
|
|
18
|
+
* type: 'view',
|
|
19
|
+
* EditPage: (props) => (
|
|
20
|
+
* <DesignerEditorWrapper
|
|
21
|
+
* {...props}
|
|
22
|
+
* renderDesigner={(value, onChange, readOnly) => (
|
|
23
|
+
* <ObjectViewConfigurator config={value} onChange={onChange} readOnly={readOnly} />
|
|
24
|
+
* )}
|
|
25
|
+
* />
|
|
26
|
+
* ),
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* The wrapper handles:
|
|
30
|
+
* • Initial fetch via `client.layered()` (so admins see overlay vs code).
|
|
31
|
+
* • Local edit state with Save / Revert.
|
|
32
|
+
* • Destructive-change confirmation (409 → dialog → retry with force).
|
|
33
|
+
* • Reset overlay button (DELETE).
|
|
34
|
+
* • Read-only fallback when the type isn't allowed to override at runtime
|
|
35
|
+
* (driven by /meta/types#allowOrgOverride).
|
|
36
|
+
*
|
|
37
|
+
* Validation errors (422) are surfaced as an error banner; bespoke
|
|
38
|
+
* designers usually handle their own field-level UX.
|
|
39
|
+
*/
|
|
40
|
+
import * as React from 'react';
|
|
41
|
+
import { useNavigate } from 'react-router-dom';
|
|
42
|
+
import { Save, Loader2, RotateCcw, History as HistoryIcon, AlertTriangle, RefreshCw, } from 'lucide-react';
|
|
43
|
+
import { Button } from '@object-ui/components';
|
|
44
|
+
import { Badge } 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, detectLocale } from './i18n';
|
|
50
|
+
export function DesignerEditorWrapper({ type, name, renderDesigner, fromMetadata, toMetadata, }) {
|
|
51
|
+
const navigate = useNavigate();
|
|
52
|
+
const client = useMetadataClient();
|
|
53
|
+
const { entries } = useMetadataTypes(client);
|
|
54
|
+
const entry = entries.find((t) => t.type === type);
|
|
55
|
+
const resolved = resolveResourceConfig(type, entry);
|
|
56
|
+
const writable = !!resolved.allowOrgOverride;
|
|
57
|
+
const locale = detectLocale();
|
|
58
|
+
const [value, setValue] = React.useState(null);
|
|
59
|
+
const [original, setOriginal] = React.useState(null);
|
|
60
|
+
const [loading, setLoading] = React.useState(true);
|
|
61
|
+
const [saving, setSaving] = React.useState(false);
|
|
62
|
+
const [error, setError] = React.useState(null);
|
|
63
|
+
const [destructive, setDestructive] = React.useState(null);
|
|
64
|
+
const dirty = React.useMemo(() => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(value) !== JSON.stringify(original);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}, [value, original]);
|
|
72
|
+
const load = React.useCallback(async () => {
|
|
73
|
+
setLoading(true);
|
|
74
|
+
setError(null);
|
|
75
|
+
try {
|
|
76
|
+
const lay = await client.layered(type, name);
|
|
77
|
+
const raw = (lay.effective ?? lay.code ?? {});
|
|
78
|
+
const v = (fromMetadata ? fromMetadata(raw) : raw);
|
|
79
|
+
setValue(v);
|
|
80
|
+
setOriginal(v);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
setError(err?.message ?? String(err));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}, [client, type, name, fromMetadata]);
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
void load();
|
|
91
|
+
}, [load]);
|
|
92
|
+
async function doSave(force, pending) {
|
|
93
|
+
const payload = pending ?? value;
|
|
94
|
+
if (payload == null)
|
|
95
|
+
return;
|
|
96
|
+
setSaving(true);
|
|
97
|
+
setError(null);
|
|
98
|
+
try {
|
|
99
|
+
const body = toMetadata ? toMetadata(payload) : payload;
|
|
100
|
+
// Ensure `name` is set — designers may not carry it.
|
|
101
|
+
const finalBody = body && typeof body === 'object'
|
|
102
|
+
? { ...body, name: body.name ?? name }
|
|
103
|
+
: body;
|
|
104
|
+
await client.save(type, name, finalBody, { force });
|
|
105
|
+
await load();
|
|
106
|
+
setDestructive(null);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err?.status === 409 && err?.code === 'destructive_change') {
|
|
110
|
+
const issues = err?.body?.issues ?? [];
|
|
111
|
+
setDestructive({ issues: Array.isArray(issues) ? issues : [], pending: payload });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
setError(err?.message ?? String(err));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
setSaving(false);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function doReset() {
|
|
122
|
+
if (!confirm(t('engine.edit.resetConfirm', locale).replace('{type}', type).replace('{name}', name)))
|
|
123
|
+
return;
|
|
124
|
+
setSaving(true);
|
|
125
|
+
try {
|
|
126
|
+
await client.reset(type, name);
|
|
127
|
+
await load();
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
setError(err?.message ?? String(err));
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
setSaving(false);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (loading || value == null) {
|
|
137
|
+
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('engine.edit.loading', locale), " ", type, "/", name, "\u2026"] }) }));
|
|
138
|
+
}
|
|
139
|
+
return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: t('engine.edit.bespokeDesigner', locale), actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: load, disabled: saving, children: [_jsx(RefreshCw, { className: "h-4 w-4 mr-1" }), " ", t('engine.list.refresh', locale)] }), _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', locale)] }), writable && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), " ", t('engine.edit.reset', locale)] })), writable && (_jsxs(Button, { size: "sm", onClick: () => doSave(false), disabled: saving || !dirty, 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', locale)] }))] }), children: [_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [!writable && (_jsx("div", { className: "mx-6 mt-4 rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: t('engine.edit.readOnlyHint', locale) })), error && (_jsxs("div", { className: "mx-6 mt-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 })] })), dirty && writable && (_jsxs("div", { className: "mx-6 mt-3 flex items-center gap-2 rounded-md border border-amber-300/50 bg-amber-50 dark:bg-amber-950/30 px-3 py-1.5 text-xs", children: [_jsx(Badge, { variant: "outline", children: t('engine.edit.unsaved', locale) }), _jsx("span", { children: t('engine.edit.unsavedHint', locale) })] })), _jsx("div", { className: "flex-1 min-h-0 overflow-auto", children: renderDesigner(value, (next) => setValue(next), !writable) })] }), _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', locale)] }), _jsx(DialogDescription, { children: t('engine.edit.destructiveHint', locale) })] }), _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', locale) }), _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', locale)] })] })] }) })] }));
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function MetadataDirectoryPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* MetadataDirectoryPage — registry-driven landing page (Phase 3c).
|
|
5
|
+
*
|
|
6
|
+
* Replaces the Phase 3b placeholder. Improvements:
|
|
7
|
+
* • Search across type id + label + description.
|
|
8
|
+
* • Domain filter chips with counts.
|
|
9
|
+
* • "Writable only" toggle for admins triaging permissions work.
|
|
10
|
+
* • Tiles show: label, machine name, item count (lazy fetched per
|
|
11
|
+
* type when in view), badges (writable / overlay), description.
|
|
12
|
+
*
|
|
13
|
+
* Item counts are lazy — we only fetch when the user lands on a
|
|
14
|
+
* domain that has it. For MVP we just show "—" until the user clicks
|
|
15
|
+
* into a type. Pre-fetching all 27 counts on page load is wasteful.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
import { Link } from 'react-router-dom';
|
|
19
|
+
import { Search, Database, Layers, Workflow, Sparkles, Settings, ShieldCheck, Box } from 'lucide-react';
|
|
20
|
+
import { Input } from '@object-ui/components';
|
|
21
|
+
import { Button } from '@object-ui/components';
|
|
22
|
+
import { Badge } from '@object-ui/components';
|
|
23
|
+
import { Kbd } from '@object-ui/components';
|
|
24
|
+
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
25
|
+
import { useMetadataClient, useMetadataTypes, } from './useMetadata';
|
|
26
|
+
import { MetadataQuickFind } from './QuickFind';
|
|
27
|
+
import { translateMetadataType, translateMetadataDomain, t, tFormat, detectLocale, } from './i18n';
|
|
28
|
+
const DOMAIN_ICONS = {
|
|
29
|
+
data: Database,
|
|
30
|
+
ui: Layers,
|
|
31
|
+
automation: Workflow,
|
|
32
|
+
ai: Sparkles,
|
|
33
|
+
system: Settings,
|
|
34
|
+
platform: Settings,
|
|
35
|
+
identity: ShieldCheck,
|
|
36
|
+
security: ShieldCheck,
|
|
37
|
+
};
|
|
38
|
+
const DOMAIN_ORDER = [
|
|
39
|
+
'data',
|
|
40
|
+
'ui',
|
|
41
|
+
'automation',
|
|
42
|
+
'ai',
|
|
43
|
+
'identity',
|
|
44
|
+
'security',
|
|
45
|
+
'system',
|
|
46
|
+
'platform',
|
|
47
|
+
'other',
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Types intentionally hidden from the directory + list views.
|
|
51
|
+
*
|
|
52
|
+
* `field` is managed in-context via the parent object's edit form
|
|
53
|
+
* (master-detail widget). A flat global list of every field across
|
|
54
|
+
* every object is rarely useful and clutters the admin surface.
|
|
55
|
+
*/
|
|
56
|
+
const HIDDEN_TYPES = new Set(['field']);
|
|
57
|
+
export function MetadataDirectoryPage() {
|
|
58
|
+
const client = useMetadataClient();
|
|
59
|
+
const { loading, error, entries } = useMetadataTypes(client);
|
|
60
|
+
const locale = React.useMemo(() => detectLocale(), []);
|
|
61
|
+
const [query, setQuery] = React.useState('');
|
|
62
|
+
const [domainFilter, setDomainFilter] = React.useState('all');
|
|
63
|
+
const [writableOnly, setWritableOnly] = React.useState(false);
|
|
64
|
+
// Counts per domain for the filter chip bar.
|
|
65
|
+
const domainCounts = React.useMemo(() => {
|
|
66
|
+
const visible = entries.filter((e) => !HIDDEN_TYPES.has(e.type));
|
|
67
|
+
const c = { all: visible.length };
|
|
68
|
+
for (const e of visible) {
|
|
69
|
+
const d = e.domain ?? 'other';
|
|
70
|
+
c[d] = (c[d] ?? 0) + 1;
|
|
71
|
+
}
|
|
72
|
+
return c;
|
|
73
|
+
}, [entries]);
|
|
74
|
+
const writableCount = entries.filter((e) => !HIDDEN_TYPES.has(e.type) && e.allowOrgOverride).length;
|
|
75
|
+
const filtered = entries.filter((e) => {
|
|
76
|
+
if (HIDDEN_TYPES.has(e.type))
|
|
77
|
+
return false;
|
|
78
|
+
if (writableOnly && !e.allowOrgOverride)
|
|
79
|
+
return false;
|
|
80
|
+
if (domainFilter !== 'all' && (e.domain ?? 'other') !== domainFilter)
|
|
81
|
+
return false;
|
|
82
|
+
if (query) {
|
|
83
|
+
const q = query.toLowerCase();
|
|
84
|
+
const hit = e.type.toLowerCase().includes(q) ||
|
|
85
|
+
(e.label ?? '').toLowerCase().includes(q) ||
|
|
86
|
+
(e.description ?? '').toLowerCase().includes(q);
|
|
87
|
+
if (!hit)
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
// Group filtered by domain for display.
|
|
93
|
+
const grouped = React.useMemo(() => {
|
|
94
|
+
var _a;
|
|
95
|
+
const map = {};
|
|
96
|
+
for (const e of filtered)
|
|
97
|
+
(map[_a = e.domain ?? 'other'] ?? (map[_a] = [])).push(e);
|
|
98
|
+
return Object.entries(map).sort(([a], [b]) => {
|
|
99
|
+
const ai = DOMAIN_ORDER.indexOf(a);
|
|
100
|
+
const bi = DOMAIN_ORDER.indexOf(b);
|
|
101
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
102
|
+
});
|
|
103
|
+
}, [filtered]);
|
|
104
|
+
if (loading) {
|
|
105
|
+
return _jsx("div", { className: "p-6 text-sm text-muted-foreground", children: t('engine.directory.loading', locale) });
|
|
106
|
+
}
|
|
107
|
+
if (error) {
|
|
108
|
+
return (_jsxs("div", { className: "p-6 text-sm text-destructive", children: [t('engine.directory.loadFailed', locale), ": ", error] }));
|
|
109
|
+
}
|
|
110
|
+
return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "px-6 pt-5 pb-4 border-b bg-background", children: [_jsx("h1", { className: "text-xl font-semibold", children: t('engine.directory.title', locale) }), _jsx("p", { className: "text-sm text-muted-foreground mt-1 max-w-3xl", dangerouslySetInnerHTML: {
|
|
111
|
+
__html: tFormat('engine.directory.description', locale, {
|
|
112
|
+
count: `<strong class="text-foreground">${entries.filter((e) => !HIDDEN_TYPES.has(e.type)).length}</strong>`,
|
|
113
|
+
writable: writableCount,
|
|
114
|
+
}),
|
|
115
|
+
} }), _jsxs("div", { className: "flex items-center gap-3 mt-4 flex-wrap", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-lg", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" }), _jsx(Input, { className: "pl-8", placeholder: t('engine.directory.search', locale), value: query, onChange: (e) => setQuery(e.target.value) })] }), _jsxs(Button, { variant: writableOnly ? 'default' : 'outline', size: "sm", onClick: () => setWritableOnly((w) => !w), children: [t('engine.directory.writableOnly', locale), " (", writableCount, ")"] }), _jsxs("div", { className: "text-xs text-muted-foreground flex items-center gap-1.5", children: [t('engine.directory.quickFind', locale), " ", _jsx(Kbd, { children: "\u2318" }), _jsx(Kbd, { children: "\u21E7" }), _jsx(Kbd, { children: "M" })] })] }), _jsxs("div", { className: "flex items-center gap-1.5 mt-3 flex-wrap", children: [_jsx(DomainChip, { domain: "all", label: t('engine.directory.all', locale), active: domainFilter === 'all', count: domainCounts.all ?? 0, onClick: () => setDomainFilter('all') }), DOMAIN_ORDER.filter((d) => domainCounts[d]).map((d) => (_jsx(DomainChip, { domain: d, label: translateMetadataDomain(d, locale), active: domainFilter === d, count: domainCounts[d] ?? 0, onClick: () => setDomainFilter(d) }, d)))] })] }), _jsxs("div", { className: "flex-1 overflow-auto p-6 space-y-6", children: [filtered.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: t('engine.directory.noMatches', locale) }), _jsx(EmptyDescription, { children: t('engine.directory.noMatchesHint', locale) })] })), grouped.map(([domain, group]) => {
|
|
116
|
+
const Icon = DOMAIN_ICONS[domain] ?? Box;
|
|
117
|
+
return (_jsxs("section", { className: "space-y-2", children: [_jsxs("h2", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5", children: [_jsx(Icon, { className: "h-3.5 w-3.5" }), translateMetadataDomain(domain, locale), " (", group.length, ")"] }), _jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3", children: group.map((e) => (_jsx(TypeTile, { entry: e, locale: locale }, e.type))) })] }, domain));
|
|
118
|
+
})] }), _jsx(MetadataQuickFind, {})] }));
|
|
119
|
+
}
|
|
120
|
+
function DomainChip({ domain, label, active, count, onClick, }) {
|
|
121
|
+
return (_jsxs("button", { type: "button", onClick: onClick, className: 'px-2.5 py-1 rounded-full text-xs border transition-colors ' +
|
|
122
|
+
(active
|
|
123
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
124
|
+
: 'bg-background hover:bg-accent border-border text-muted-foreground'), children: [label ?? domain, " ", _jsxs("span", { className: "opacity-70 ml-0.5", children: ["(", count, ")"] })] }));
|
|
125
|
+
}
|
|
126
|
+
function TypeTile({ entry, locale }) {
|
|
127
|
+
// Prefer the locale-table translation; fall back to server's `label` (typically English).
|
|
128
|
+
const label = translateMetadataType(entry.type, locale, entry.label);
|
|
129
|
+
return (_jsxs(Link, { to: `../component/metadata/resource?type=${encodeURIComponent(entry.type)}`, className: "block p-4 border rounded-lg hover:border-primary hover:bg-accent transition-colors", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-medium truncate", children: label }), _jsx("code", { className: "text-xs text-muted-foreground font-mono", children: entry.type })] }), entry.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] shrink-0 ' +
|
|
130
|
+
(entry.overrideSource === 'env'
|
|
131
|
+
? 'bg-amber-100 text-amber-800 hover:bg-amber-100'
|
|
132
|
+
: 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), title: entry.overrideSource === 'env'
|
|
133
|
+
? 'Writable via OBJECTSTACK_METADATA_WRITABLE env var'
|
|
134
|
+
: 'Writable per ADR-0005 overlay opt-in', children: "writable" })) : (_jsx(Badge, { variant: "outline", className: "text-[10px] shrink-0 text-muted-foreground", children: "read-only" }))] }), entry.description && (_jsx("div", { className: "text-xs text-muted-foreground mt-2 line-clamp-2", children: entry.description }))] }));
|
|
135
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MetadataLayered } from '@object-ui/data-objectstack';
|
|
2
|
+
export interface LayeredDiffProps {
|
|
3
|
+
layered: MetadataLayered<Record<string, unknown>> | null;
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function LayeredDiff({ layered, loading }: LayeredDiffProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@object-ui/components';
|
|
3
|
+
import { Badge } from '@object-ui/components';
|
|
4
|
+
export function LayeredDiff({ layered, loading }) {
|
|
5
|
+
if (loading) {
|
|
6
|
+
return (_jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading layers\u2026" }));
|
|
7
|
+
}
|
|
8
|
+
if (!layered) {
|
|
9
|
+
return (_jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Layered view unavailable for this item." }));
|
|
10
|
+
}
|
|
11
|
+
const hasOverlay = layered.overlay != null;
|
|
12
|
+
return (_jsxs(Tabs, { defaultValue: "effective", className: "w-full", children: [_jsxs(TabsList, { className: "grid grid-cols-3 w-fit", children: [_jsxs(TabsTrigger, { value: "code", children: ["Code", _jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: "artifact" })] }), _jsxs(TabsTrigger, { value: "overlay", children: ["Overlay", hasOverlay ? (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: layered.overlayScope ?? 'set' })) : (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px] text-muted-foreground", children: "none" }))] }), _jsxs(TabsTrigger, { value: "effective", children: ["Effective", _jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: "merged" })] })] }), _jsx(TabsContent, { value: "code", className: "mt-3", children: _jsx(LayerPanel, { payload: layered.code, emptyHint: "No code-level artifact for this item. It may have been created at runtime as a pure overlay." }) }), _jsx(TabsContent, { value: "overlay", className: "mt-3", children: _jsx(LayerPanel, { payload: layered.overlay, emptyHint: "No overlay set. The runtime is using the code-level value as-is." }) }), _jsx(TabsContent, { value: "effective", className: "mt-3", children: _jsx(LayerPanel, { payload: layered.effective, emptyHint: "No effective value resolved." }) })] }));
|
|
13
|
+
}
|
|
14
|
+
function LayerPanel({ payload, emptyHint, }) {
|
|
15
|
+
if (payload == null) {
|
|
16
|
+
return (_jsx("div", { className: "rounded border bg-muted/30 p-4 text-xs text-muted-foreground", children: emptyHint }));
|
|
17
|
+
}
|
|
18
|
+
let pretty;
|
|
19
|
+
try {
|
|
20
|
+
pretty = JSON.stringify(payload, null, 2);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
pretty = String(payload);
|
|
24
|
+
}
|
|
25
|
+
return (_jsx("pre", { className: "rounded border bg-muted/30 p-3 text-xs font-mono overflow-auto max-h-[420px]", children: pretty }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PageShell — common hero / breadcrumb / action bar for every metadata
|
|
3
|
+
* admin page (Phase 3c).
|
|
4
|
+
*
|
|
5
|
+
* Layout (top → bottom):
|
|
6
|
+
* 1. Breadcrumb: All Metadata Types › <type> [ › <item> ]
|
|
7
|
+
* 2. Hero: icon + label + writable badge + count chip
|
|
8
|
+
* 3. Description (one line)
|
|
9
|
+
* 4. Action toolbar (Create / Refresh / Reset / Delete)
|
|
10
|
+
* 5. Child content (list / form / history)
|
|
11
|
+
*
|
|
12
|
+
* Keeping the chrome here means every page looks consistent and
|
|
13
|
+
* page bodies stay focused on their domain logic.
|
|
14
|
+
*/
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import type { RichMetadataTypeEntry } from './useMetadata';
|
|
17
|
+
export interface PageShellProps {
|
|
18
|
+
/** The type entry from `/meta/types` (or a synthesized stub). */
|
|
19
|
+
entry: RichMetadataTypeEntry | undefined;
|
|
20
|
+
/** Optional item name (shown in breadcrumb on edit/history). */
|
|
21
|
+
itemName?: string;
|
|
22
|
+
/** Sub-label below the title — e.g. "Edit overlay" / "Version history". */
|
|
23
|
+
subtitle?: string;
|
|
24
|
+
/** Right-side stat chips. */
|
|
25
|
+
stats?: Array<{
|
|
26
|
+
label: string;
|
|
27
|
+
value: React.ReactNode;
|
|
28
|
+
}>;
|
|
29
|
+
/** Right-side action buttons. */
|
|
30
|
+
actions?: React.ReactNode;
|
|
31
|
+
/** Page body. */
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
export declare function PageShell({ entry, itemName, subtitle, stats, actions, children, }: PageShellProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
|
+
/**
|
|
4
|
+
* PageShell — common hero / breadcrumb / action bar for every metadata
|
|
5
|
+
* admin page (Phase 3c).
|
|
6
|
+
*
|
|
7
|
+
* Layout (top → bottom):
|
|
8
|
+
* 1. Breadcrumb: All Metadata Types › <type> [ › <item> ]
|
|
9
|
+
* 2. Hero: icon + label + writable badge + count chip
|
|
10
|
+
* 3. Description (one line)
|
|
11
|
+
* 4. Action toolbar (Create / Refresh / Reset / Delete)
|
|
12
|
+
* 5. Child content (list / form / history)
|
|
13
|
+
*
|
|
14
|
+
* Keeping the chrome here means every page looks consistent and
|
|
15
|
+
* page bodies stay focused on their domain logic.
|
|
16
|
+
*/
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
import { Link } from 'react-router-dom';
|
|
19
|
+
import { Badge } from '@object-ui/components';
|
|
20
|
+
import { ChevronRight } from 'lucide-react';
|
|
21
|
+
import { detectLocale, t, translateMetadataType } from './i18n';
|
|
22
|
+
export function PageShell({ entry, itemName, subtitle, stats, actions, children, }) {
|
|
23
|
+
const type = entry?.type ?? '';
|
|
24
|
+
const locale = React.useMemo(() => detectLocale(), []);
|
|
25
|
+
// Prefer locale-table translation over server's English label.
|
|
26
|
+
const label = translateMetadataType(type, locale, entry?.label);
|
|
27
|
+
return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [_jsxs("div", { className: "px-6 pt-5 pb-4 border-b bg-background", children: [_jsxs("nav", { className: "flex items-center gap-1.5 text-xs text-muted-foreground mb-2", children: [_jsx(Link, { to: "../component/metadata/directory", className: "hover:text-foreground", children: t('engine.breadcrumb.allTypes', locale) }), _jsx(ChevronRight, { className: "h-3 w-3" }), _jsx(Link, { to: `../component/metadata/resource?type=${encodeURIComponent(type)}`, className: "hover:text-foreground", children: label }), itemName && (_jsxs(_Fragment, { children: [_jsx(ChevronRight, { className: "h-3 w-3" }), _jsx("span", { className: "text-foreground font-medium font-mono", children: itemName })] }))] }), _jsxs("div", { className: "flex items-start justify-between gap-3 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-xl font-semibold truncate", children: label }), _jsx("code", { className: "text-xs font-mono text-muted-foreground", children: type }), entry?.domain && (_jsx(Badge, { variant: "outline", className: "text-[10px] uppercase tracking-wider", children: entry.domain })), entry?.allowOrgOverride ? (_jsx(Badge, { className: 'text-[10px] ' +
|
|
28
|
+
(entry.overrideSource === 'env'
|
|
29
|
+
? 'bg-amber-100 text-amber-800 hover:bg-amber-100'
|
|
30
|
+
: 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), title: entry.overrideSource === 'env'
|
|
31
|
+
? 'Writable via OBJECTSTACK_METADATA_WRITABLE env var'
|
|
32
|
+
: 'Writable per ADR-0005 overlay opt-in', children: "writable" })) : (_jsx(Badge, { variant: "outline", className: "text-[10px] text-muted-foreground", children: "read-only" }))] }), subtitle && (_jsx("div", { className: "text-sm text-muted-foreground mt-1", children: subtitle })), !subtitle && entry?.description && (_jsx("div", { className: "text-sm text-muted-foreground mt-1 max-w-3xl", children: entry.description }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [stats?.map((s, i) => (_jsxs("div", { className: "flex flex-col items-end px-3 py-1 rounded border bg-muted/30 min-w-[64px]", children: [_jsx("span", { className: "text-[10px] uppercase tracking-wider text-muted-foreground", children: s.label }), _jsx("span", { className: "text-sm font-semibold tabular-nums", children: s.value })] }, i))), actions] })] })] }), _jsx("div", { className: "flex-1 overflow-auto", children: children })] }));
|
|
33
|
+
}
|