@object-ui/app-shell 6.1.0 → 6.2.1

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 (87) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +10 -1
  3. package/dist/console/AppContent.js +53 -2
  4. package/dist/console/ai/AiChatPage.d.ts +8 -0
  5. package/dist/console/ai/AiChatPage.js +188 -0
  6. package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
  7. package/dist/console/ai/ConversationsSidebar.js +111 -0
  8. package/dist/console/auth/LoginPage.js +19 -2
  9. package/dist/console/auth/RegisterPage.js +30 -1
  10. package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
  11. package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
  12. package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
  13. package/dist/console/marketplace/MarketplacePage.js +55 -18
  14. package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
  15. package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
  16. package/dist/console/marketplace/usePackageL10n.js +110 -0
  17. package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
  18. package/dist/console/organizations/OrganizationsPage.js +24 -3
  19. package/dist/context/FavoritesProvider.d.ts +40 -2
  20. package/dist/context/FavoritesProvider.js +201 -20
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/index.js +1 -0
  23. package/dist/hooks/useChatConversation.d.ts +7 -0
  24. package/dist/hooks/useChatConversation.js +37 -5
  25. package/dist/hooks/useConversationList.d.ts +25 -0
  26. package/dist/hooks/useConversationList.js +131 -0
  27. package/dist/hooks/useNavPins.d.ts +11 -4
  28. package/dist/hooks/useNavPins.js +104 -53
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +14 -0
  31. package/dist/layout/AppHeader.js +2 -2
  32. package/dist/layout/AppSidebar.js +20 -1
  33. package/dist/layout/UnifiedSidebar.js +1 -1
  34. package/dist/providers/ExpressionProvider.d.ts +11 -1
  35. package/dist/providers/ExpressionProvider.js +11 -6
  36. package/dist/services/builtinComponents.d.ts +1 -0
  37. package/dist/services/builtinComponents.js +169 -0
  38. package/dist/services/componentRegistry.d.ts +63 -0
  39. package/dist/services/componentRegistry.js +36 -0
  40. package/dist/views/ComponentNavView.d.ts +6 -0
  41. package/dist/views/ComponentNavView.js +26 -0
  42. package/dist/views/RecordDetailView.js +66 -6
  43. package/dist/views/RecordFormPage.js +15 -3
  44. package/dist/views/SearchResultsPage.js +4 -0
  45. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +68 -0
  46. package/dist/views/metadata-admin/DesignerEditorWrapper.js +158 -0
  47. package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
  48. package/dist/views/metadata-admin/DirectoryPage.js +135 -0
  49. package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
  50. package/dist/views/metadata-admin/LayeredDiff.js +26 -0
  51. package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
  52. package/dist/views/metadata-admin/MetadataDetailDrawer.js +52 -0
  53. package/dist/views/metadata-admin/PageShell.d.ts +34 -0
  54. package/dist/views/metadata-admin/PageShell.js +40 -0
  55. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
  56. package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
  57. package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
  58. package/dist/views/metadata-admin/QuickFind.js +152 -0
  59. package/dist/views/metadata-admin/RelatedPanel.d.ts +33 -0
  60. package/dist/views/metadata-admin/RelatedPanel.js +171 -0
  61. package/dist/views/metadata-admin/ResourceEditPage.d.ts +13 -0
  62. package/dist/views/metadata-admin/ResourceEditPage.js +302 -0
  63. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/ResourceHistoryPage.js +100 -0
  65. package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
  66. package/dist/views/metadata-admin/ResourceListPage.js +146 -0
  67. package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
  68. package/dist/views/metadata-admin/ResourceRouter.js +47 -0
  69. package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
  70. package/dist/views/metadata-admin/SchemaForm.js +565 -0
  71. package/dist/views/metadata-admin/anchors.d.ts +1 -0
  72. package/dist/views/metadata-admin/anchors.js +229 -0
  73. package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
  74. package/dist/views/metadata-admin/default-schemas.js +207 -0
  75. package/dist/views/metadata-admin/i18n.d.ts +33 -0
  76. package/dist/views/metadata-admin/i18n.js +303 -0
  77. package/dist/views/metadata-admin/index.d.ts +33 -0
  78. package/dist/views/metadata-admin/index.js +39 -0
  79. package/dist/views/metadata-admin/predicate.d.ts +31 -0
  80. package/dist/views/metadata-admin/predicate.js +150 -0
  81. package/dist/views/metadata-admin/registry.d.ts +232 -0
  82. package/dist/views/metadata-admin/registry.js +106 -0
  83. package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
  84. package/dist/views/metadata-admin/useMetadata.js +96 -0
  85. package/dist/views/metadata-admin/widgets.d.ts +68 -0
  86. package/dist/views/metadata-admin/widgets.js +287 -0
  87. package/package.json +27 -26
@@ -0,0 +1,171 @@
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
+ * RelatedPanel — lists metadata items anchored to the current parent.
5
+ *
6
+ * Driven by the anchor registry (see `anchors.ts` / `registry.ts`). For
7
+ * each child type that declares it anchors at `parentType`, we issue
8
+ * one `client.list(childType)` call in parallel, filter the result with
9
+ * the anchor's match predicate, and render the survivors in collapsible
10
+ * groups.
11
+ *
12
+ * Why client-side filter? Anchoring lives in metadata bodies that are
13
+ * not indexed on the server today, and adding a parameterised list API
14
+ * for every potential anchor field would balloon the surface area. The
15
+ * Related tab is opened by humans on a specific object, so the cost of
16
+ * pulling 100-or-so items per child type is negligible — far cheaper
17
+ * than a network round-trip per row would have been.
18
+ *
19
+ * Visual model:
20
+ * - One <details> per group (default open if non-empty).
21
+ * - Rows are kebab-y: name, optional label, optional badge.
22
+ * - Click → call `onOpen({ type, name })`; parent owns the drawer.
23
+ * - A search input filters across all groups by name/label substring.
24
+ */
25
+ import * as React from 'react';
26
+ import { useNavigate } from 'react-router-dom';
27
+ import { Loader2, Search, Plus, ChevronRight, ExternalLink } from 'lucide-react';
28
+ import { Badge, Button, Input, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
29
+ import { useMetadataClient } from './useMetadata';
30
+ import { listAnchorsFor } from './registry';
31
+ export function RelatedPanel({ type, name, parentItem, onOpen, }) {
32
+ const client = useMetadataClient();
33
+ const navigate = useNavigate();
34
+ const anchors = React.useMemo(() => listAnchorsFor(type), [type]);
35
+ const [groups, setGroups] = React.useState([]);
36
+ const [search, setSearch] = React.useState('');
37
+ const [collapsed, setCollapsed] = React.useState({});
38
+ React.useEffect(() => {
39
+ if (anchors.length === 0) {
40
+ setGroups([]);
41
+ return;
42
+ }
43
+ let cancelled = false;
44
+ // Seed groups in loading state, ordered by registry order. Embedded
45
+ // groups resolve synchronously from `parentItem`; list groups dispatch
46
+ // a `client.list(childType)` and filter.
47
+ const initial = [...anchors]
48
+ .sort((a, b) => (a.anchor.order ?? 999) - (b.anchor.order ?? 999))
49
+ .map((entry) => {
50
+ const isEmbedded = entry.anchor.source === 'embedded';
51
+ if (isEmbedded) {
52
+ const raw = entry.anchor.extract && parentItem
53
+ ? entry.anchor.extract(parentItem)
54
+ : [];
55
+ return {
56
+ childType: entry.type,
57
+ anchor: entry.anchor,
58
+ loading: false,
59
+ error: null,
60
+ items: raw.map(normaliseItem),
61
+ };
62
+ }
63
+ return {
64
+ childType: entry.type,
65
+ anchor: entry.anchor,
66
+ loading: true,
67
+ error: null,
68
+ items: [],
69
+ };
70
+ });
71
+ setGroups(initial);
72
+ void Promise.all(initial.map(async (g) => {
73
+ if (g.anchor.source === 'embedded')
74
+ return g;
75
+ try {
76
+ const list = (await client.list(g.childType));
77
+ if (cancelled)
78
+ return g;
79
+ const matchFn = g.anchor.match ?? (() => false);
80
+ const filtered = list
81
+ .filter((item) => matchFn(item, name))
82
+ .map((item) => normaliseItem(item));
83
+ return { ...g, loading: false, items: filtered };
84
+ }
85
+ catch (err) {
86
+ if (cancelled)
87
+ return g;
88
+ return {
89
+ ...g,
90
+ loading: false,
91
+ error: err instanceof Error ? err.message : String(err),
92
+ };
93
+ }
94
+ })).then((finished) => {
95
+ if (cancelled)
96
+ return;
97
+ setGroups(finished);
98
+ });
99
+ return () => {
100
+ cancelled = true;
101
+ };
102
+ }, [client, type, name, anchors, parentItem]);
103
+ if (anchors.length === 0) {
104
+ return (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No related metadata" }), _jsxs(EmptyDescription, { children: ["No metadata types are configured to anchor at ", _jsx("code", { children: type }), ". You can register one via", ' ', _jsx("code", { className: "font-mono", children: "registerMetadataResource" }), "."] })] }));
105
+ }
106
+ const totalCount = groups.reduce((sum, g) => sum + g.items.length, 0);
107
+ const anyLoading = groups.some((g) => g.loading);
108
+ const q = search.trim().toLowerCase();
109
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "relative flex-1 max-w-sm", children: [_jsx(Search, { className: "absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Search related\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-8 h-8 text-sm" })] }), _jsx("div", { className: "text-xs text-muted-foreground", children: anyLoading
110
+ ? 'Scanning…'
111
+ : `${totalCount} item${totalCount === 1 ? '' : 's'}` })] }), groups.map((g) => {
112
+ const matches = q
113
+ ? g.items.filter((it) => it.name.toLowerCase().includes(q) ||
114
+ (it.label ?? '').toLowerCase().includes(q))
115
+ : g.items;
116
+ const isCollapsed = collapsed[g.childType] ?? false;
117
+ const visible = !q || matches.length > 0;
118
+ if (!visible && !g.loading)
119
+ return null;
120
+ return (_jsxs("div", { className: "border rounded-lg overflow-hidden", children: [_jsxs("div", { className: "w-full flex items-center gap-2 px-3 py-2 bg-muted/40", children: [_jsxs("button", { type: "button", className: "flex items-center gap-2 flex-1 text-left hover:opacity-80", onClick: () => setCollapsed((s) => ({ ...s, [g.childType]: !isCollapsed })), children: [_jsx(ChevronRight, { className: cn('h-4 w-4 text-muted-foreground transition-transform', !isCollapsed && 'rotate-90') }), _jsx("div", { className: "text-sm font-medium", children: g.anchor.groupLabel ?? g.childType }), _jsx(Badge, { variant: "outline", className: "text-[10px]", children: g.loading ? '…' : matches.length })] }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-7 px-2", onClick: () => {
121
+ if (g.anchor.source === 'embedded') {
122
+ // Embedded items are edited inside the parent's Form
123
+ // tab; jump there rather than to a nonexistent route.
124
+ if (typeof window !== 'undefined') {
125
+ const url = new URL(window.location.href);
126
+ url.searchParams.set('tab', 'form');
127
+ url.searchParams.delete('open');
128
+ window.location.assign(url.toString());
129
+ }
130
+ return;
131
+ }
132
+ navigate(`../../${encodeURIComponent(g.childType)}/_new?anchor=${encodeURIComponent(`${type}:${name}`)}`);
133
+ }, title: g.anchor.source === 'embedded'
134
+ ? `Edit in Form tab`
135
+ : `New ${g.childType}`, children: _jsx(Plus, { className: "h-3.5 w-3.5" }) })] }), !isCollapsed && (_jsxs("div", { className: "divide-y", children: [g.loading && (_jsxs("div", { className: "px-3 py-3 text-xs text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" }), "Loading ", g.childType, "\u2026"] })), !g.loading && g.error && (_jsxs("div", { className: "px-3 py-3 text-xs text-destructive", children: ["Failed: ", g.error] })), !g.loading && !g.error && matches.length === 0 && (_jsx("div", { className: "px-3 py-3 text-xs text-muted-foreground", children: q ? 'No matches.' : 'Nothing here yet.' })), !g.loading &&
136
+ !g.error &&
137
+ matches.map((it, idx) => (_jsxs("button", { type: "button", onClick: () => onOpen(g.anchor.source === 'embedded'
138
+ ? {
139
+ kind: 'embedded',
140
+ parentType: type,
141
+ parentName: name,
142
+ groupLabel: g.anchor.groupLabel ?? g.childType,
143
+ itemName: it.name,
144
+ raw: it.raw,
145
+ }
146
+ : {
147
+ kind: 'metadata',
148
+ type: g.childType,
149
+ name: it.name,
150
+ }), className: "w-full text-left px-3 py-2 hover:bg-accent/50 flex items-center gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "font-mono text-xs truncate", children: it.name }), it.label && it.label !== it.name && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: it.label }))] }), _jsx(ExternalLink, { className: "h-3.5 w-3.5 text-muted-foreground" })] }, `${it.name}-${idx}`)))] }))] }, g.childType));
151
+ })] }));
152
+ }
153
+ function normaliseItem(raw) {
154
+ const nameVal = pickString(raw, ['name', 'id', 'key']) ?? '(unnamed)';
155
+ const labelVal = pickString(raw, ['label', 'title', 'displayName']);
156
+ const descVal = pickString(raw, ['description', 'summary']);
157
+ return {
158
+ name: nameVal,
159
+ label: labelVal,
160
+ description: descVal,
161
+ raw,
162
+ };
163
+ }
164
+ function pickString(obj, keys) {
165
+ for (const k of keys) {
166
+ const v = obj[k];
167
+ if (typeof v === 'string' && v.length > 0)
168
+ return v;
169
+ }
170
+ return undefined;
171
+ }
@@ -0,0 +1,13 @@
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
+ * When true, the editor is rendered inside another surface (e.g.
8
+ * the Related drawer). Hides Related-tab and URL-sync so the inner
9
+ * page does not fight the outer page for `?tab` / `?open`.
10
+ */
11
+ embedded?: boolean;
12
+ }
13
+ export declare function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode, embedded, }: MetadataResourceEditPageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,302 @@
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
+ * MetadataResourceEditPage — generic AutoForm-driven editor (Phase 3c).
5
+ *
6
+ * What it does:
7
+ * 1. Fetches the layered view (`?layers=true`) so the user sees code
8
+ * vs overlay vs effective.
9
+ * 2. Renders a SchemaForm against the JSONSchema in the type's
10
+ * `/meta/types` registry row.
11
+ * 3. Save → PUT, with automatic destructive-change handling: a 409
12
+ * `destructive_change` response opens a confirmation dialog
13
+ * listing the issues, and on confirm we retry with `?force=true`.
14
+ * 4. Reset overlay → DELETE (overlay only).
15
+ * 5. References tab → calls `client.references()` and lists
16
+ * back-pointers so admins know what will break before deleting.
17
+ *
18
+ * Works for any of the 27 metadata types — bespoke editors (Object,
19
+ * Field, View, Permission Matrix) opt out by registering a custom
20
+ * EditPage via `registerMetadataResource()`.
21
+ */
22
+ import * as React from 'react';
23
+ import { useNavigate, useParams } from 'react-router-dom';
24
+ import { Save, RotateCcw, History, Link2, Loader2, AlertTriangle, Layers3, } from 'lucide-react';
25
+ import { Button } from '@object-ui/components';
26
+ import { Badge } from '@object-ui/components';
27
+ import { Tabs, TabsContent, TabsList, TabsTrigger, } from '@object-ui/components';
28
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@object-ui/components';
29
+ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
30
+ import { PageShell } from './PageShell';
31
+ import { LayeredDiff } from './LayeredDiff';
32
+ import { SchemaForm } from './SchemaForm';
33
+ import { useMetadataClient, useMetadataTypes, } from './useMetadata';
34
+ import { getMetadataResource, resolveResourceConfig, listAnchorsFor, } from './registry';
35
+ import { RelatedPanel } from './RelatedPanel';
36
+ import { MetadataDetailDrawer } from './MetadataDetailDrawer';
37
+ export function MetadataResourceEditPage({ type: typeProp, name: nameProp, createMode = false, embedded = false, }) {
38
+ const params = useParams();
39
+ const type = typeProp ?? params.type ?? '';
40
+ const name = nameProp ?? params.name ?? '';
41
+ const navigate = useNavigate();
42
+ const client = useMetadataClient();
43
+ const { entries } = useMetadataTypes(client);
44
+ const entry = entries.find((t) => t.type === type);
45
+ const config = resolveResourceConfig(type, entry);
46
+ // Custom editor takes over.
47
+ const customConfig = getMetadataResource(type);
48
+ if (customConfig?.EditPage && !createMode) {
49
+ const Custom = customConfig.EditPage;
50
+ return _jsx(Custom, { type: type, name: name });
51
+ }
52
+ if (customConfig?.CreatePage && createMode) {
53
+ const Custom = customConfig.CreatePage;
54
+ return _jsx(Custom, { type: type });
55
+ }
56
+ const [layered, setLayered] = React.useState(null);
57
+ const [draft, setDraft] = React.useState(createMode ? { name: '' } : {});
58
+ const [refs, setRefs] = React.useState(null);
59
+ const [loading, setLoading] = React.useState(!createMode);
60
+ const [saving, setSaving] = React.useState(false);
61
+ const [error, setError] = React.useState(null);
62
+ const [issues, setIssues] = React.useState([]);
63
+ const [destructiveIssues, setDestructiveIssues] = React.useState(null);
64
+ const [pendingItem, setPendingItem] = React.useState(null);
65
+ // Prefetch object name list once — fuels the `ref:object` widget.
66
+ // We don't block render on it; the widget shows a "Loading…" state.
67
+ const [objectNames, setObjectNames] = React.useState([]);
68
+ const [objectsLoading, setObjectsLoading] = React.useState(true);
69
+ React.useEffect(() => {
70
+ let cancelled = false;
71
+ (async () => {
72
+ try {
73
+ const list = (await client.list('object'));
74
+ if (cancelled)
75
+ return;
76
+ setObjectNames(list.map((x) => x?.name).filter((n) => !!n).sort());
77
+ }
78
+ catch {
79
+ if (!cancelled)
80
+ setObjectNames([]);
81
+ }
82
+ finally {
83
+ if (!cancelled)
84
+ setObjectsLoading(false);
85
+ }
86
+ })();
87
+ return () => {
88
+ cancelled = true;
89
+ };
90
+ }, [client]);
91
+ const widgetContext = React.useMemo(() => ({ objectNames, objectsLoading }), [objectNames, objectsLoading]);
92
+ // Load layered view + initial draft.
93
+ React.useEffect(() => {
94
+ if (createMode) {
95
+ setLoading(false);
96
+ return;
97
+ }
98
+ let cancelled = false;
99
+ setLoading(true);
100
+ setError(null);
101
+ (async () => {
102
+ try {
103
+ const lay = await client.layered(type, name);
104
+ if (cancelled)
105
+ return;
106
+ setLayered(lay);
107
+ // Initial draft = effective if available, otherwise code.
108
+ const initial = (lay.effective ?? lay.code ?? {});
109
+ setDraft(initial);
110
+ setLoading(false);
111
+ }
112
+ catch (err) {
113
+ if (!cancelled) {
114
+ setError(err?.message ?? String(err));
115
+ setLoading(false);
116
+ }
117
+ }
118
+ })();
119
+ return () => {
120
+ cancelled = true;
121
+ };
122
+ }, [client, type, name, createMode]);
123
+ // Lazy-load references when the tab is opened.
124
+ const [refsLoading, setRefsLoading] = React.useState(false);
125
+ async function loadReferences() {
126
+ if (refs != null || refsLoading)
127
+ return;
128
+ setRefsLoading(true);
129
+ try {
130
+ const r = await client.references(type, name);
131
+ setRefs(r);
132
+ }
133
+ catch (err) {
134
+ // Surface as empty list; non-blocking.
135
+ setRefs([]);
136
+ console.error('references() failed', err);
137
+ }
138
+ finally {
139
+ setRefsLoading(false);
140
+ }
141
+ }
142
+ // Related drawer state. `null` = closed. We avoid querystring round-
143
+ // trips on every keystroke; URL state is best-effort sync via effect
144
+ // below.
145
+ const [relatedTarget, setRelatedTarget] = React.useState(null);
146
+ const hasAnchors = React.useMemo(() => !createMode && !embedded && listAnchorsFor(type).length > 0, [type, createMode, embedded]);
147
+ // Read ?tab and ?open on first mount so deep-links work. Embedded
148
+ // items are not deep-linkable (they live in the parent body and need
149
+ // the parent payload to materialise) so we only restore metadata
150
+ // targets here.
151
+ const initialTabRef = React.useRef(null);
152
+ React.useEffect(() => {
153
+ if (typeof window === 'undefined' || embedded)
154
+ return;
155
+ const sp = new URLSearchParams(window.location.search);
156
+ const tab = sp.get('tab');
157
+ if (tab)
158
+ initialTabRef.current = tab;
159
+ const open = sp.get('open');
160
+ if (open && open.includes(':')) {
161
+ const [t, n] = open.split(':', 2);
162
+ if (t && n)
163
+ setRelatedTarget({ kind: 'metadata', type: t, name: n });
164
+ }
165
+ // intentionally empty deps — first mount only
166
+ // eslint-disable-next-line react-hooks/exhaustive-deps
167
+ }, []);
168
+ // Reflect drawer target into the URL so refresh/share works.
169
+ React.useEffect(() => {
170
+ if (typeof window === 'undefined' || embedded)
171
+ return;
172
+ const url = new URL(window.location.href);
173
+ if (relatedTarget?.kind === 'metadata') {
174
+ url.searchParams.set('open', `${relatedTarget.type}:${relatedTarget.name}`);
175
+ }
176
+ else {
177
+ url.searchParams.delete('open');
178
+ }
179
+ window.history.replaceState({}, '', url.toString());
180
+ }, [relatedTarget, embedded]);
181
+ async function doSave(force) {
182
+ setSaving(true);
183
+ setError(null);
184
+ setIssues([]);
185
+ try {
186
+ // Ensure `name` is set on create.
187
+ const itemToSave = createMode
188
+ ? { ...draft, name: String(draft.name ?? name) }
189
+ : draft;
190
+ const savedName = String(itemToSave.name ?? name);
191
+ if (!savedName) {
192
+ setError('A name is required.');
193
+ setSaving(false);
194
+ return;
195
+ }
196
+ const result = await client.save(type, savedName, itemToSave, { force });
197
+ // Refresh layered after save.
198
+ const lay = await client.layered(type, savedName);
199
+ setLayered(lay);
200
+ setDraft((lay.effective ?? itemToSave));
201
+ setDestructiveIssues(null);
202
+ setPendingItem(null);
203
+ if (createMode) {
204
+ navigate(`../${encodeURIComponent(savedName)}`);
205
+ }
206
+ }
207
+ catch (err) {
208
+ // Map destructive change → confirmation dialog.
209
+ if (err?.status === 409 && err?.code === 'destructive_change') {
210
+ const i = err?.body?.issues ?? [];
211
+ setDestructiveIssues(Array.isArray(i) ? i : []);
212
+ setPendingItem(draft);
213
+ }
214
+ // Map schema validation → inline field errors.
215
+ else if (err?.status === 422 || err?.code === 'invalid_metadata' || err?.code === 'invalid_payload') {
216
+ const i = err?.body?.issues ?? [];
217
+ let mapped = (Array.isArray(i) ? i : []).map((x) => ({
218
+ path: Array.isArray(x.path) ? x.path.join('.') : String(x.path ?? ''),
219
+ message: String(x.message ?? 'Invalid'),
220
+ }));
221
+ // Backend's invalid_metadata sometimes returns a flat string like
222
+ // "<type>/<name> failed spec validation: <path>: <message>".
223
+ // Parse it into a single inline issue + summary so users see the
224
+ // real problem instead of "0 issues".
225
+ const raw = String(err?.body?.error ?? err?.message ?? '');
226
+ if (mapped.length === 0 && raw) {
227
+ const m = raw.match(/failed spec validation:\s*(.+?):\s*(.+)$/);
228
+ if (m) {
229
+ mapped = [{ path: m[1].trim(), message: m[2].trim() }];
230
+ }
231
+ else {
232
+ mapped = [{ path: '', message: raw }];
233
+ }
234
+ }
235
+ setIssues(mapped);
236
+ if (mapped.length === 1 && !mapped[0].path) {
237
+ setError(mapped[0].message);
238
+ }
239
+ else if (mapped.length === 1) {
240
+ setError(`${mapped[0].path}: ${mapped[0].message}`);
241
+ }
242
+ else {
243
+ setError(`Validation failed (${mapped.length} issues).`);
244
+ }
245
+ }
246
+ else {
247
+ setError(err?.message ?? String(err));
248
+ }
249
+ }
250
+ finally {
251
+ setSaving(false);
252
+ }
253
+ }
254
+ async function doReset() {
255
+ if (!confirm(`Reset overlay for ${type}/${name}? Code-level value will be restored.`)) {
256
+ return;
257
+ }
258
+ setSaving(true);
259
+ setError(null);
260
+ try {
261
+ await client.reset(type, name);
262
+ const lay = await client.layered(type, name);
263
+ setLayered(lay);
264
+ setDraft((lay.effective ?? lay.code ?? {}));
265
+ }
266
+ catch (err) {
267
+ setError(err?.message ?? String(err));
268
+ }
269
+ finally {
270
+ setSaving(false);
271
+ }
272
+ }
273
+ if (loading) {
274
+ 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" }), " Loading ", type, "/", name, "\u2026"] }) }));
275
+ }
276
+ const schema = entry?.schema ??
277
+ config.defaultSchema;
278
+ const readOnly = !entry?.allowOrgOverride && !createMode;
279
+ const DesignerTab = !createMode ? customConfig?.DesignerTab : undefined;
280
+ const designerTabLabel = customConfig?.designerTabLabel ?? 'Designer';
281
+ return (_jsxs(PageShell, { entry: entry ?? { type, label: type }, itemName: createMode ? '(new)' : name, subtitle: createMode ? 'Create new' : 'Edit overlay', actions: _jsxs(_Fragment, { children: [!createMode && entry?.allowOrgOverride && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: doReset, disabled: saving, children: [_jsx(RotateCcw, { className: "h-4 w-4 mr-1" }), "Reset overlay"] })), !createMode && (_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`./history`), children: [_jsx(History, { className: "h-4 w-4 mr-1" }), "History"] })), entry?.allowOrgOverride && (_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" })), "Save"] }))] }), children: [_jsxs("div", { className: "p-6 space-y-6 max-w-7xl", children: [error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), readOnly && (_jsxs("div", { className: "text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3", children: ["This type is read-only. To enable runtime editing, set", ' ', _jsx("code", { className: "font-mono", children: "OBJECTSTACK_METADATA_WRITABLE" }), " to include ", _jsx("code", { className: "font-mono", children: type }), ", or flip", ' ', _jsx("code", { className: "font-mono", children: "allowOrgOverride" }), " in the registry."] })), _jsxs(Tabs, { defaultValue: initialTabRef.current ?? (DesignerTab ? 'designer' : 'form'), className: "w-full", onValueChange: (v) => {
282
+ if (typeof window === 'undefined' || embedded)
283
+ return;
284
+ const url = new URL(window.location.href);
285
+ url.searchParams.set('tab', v);
286
+ window.history.replaceState({}, '', url.toString());
287
+ }, children: [_jsxs(TabsList, { children: [DesignerTab && (_jsx(TabsTrigger, { value: "designer", children: designerTabLabel })), _jsx(TabsTrigger, { value: "form", children: "Form" }), !createMode && (_jsxs(TabsTrigger, { value: "layers", children: ["Layers", layered?.overlay && (_jsx(Badge, { className: "ml-1.5 text-[10px] bg-emerald-600 text-emerald-50", children: "overlay" }))] })), !createMode && (_jsxs(TabsTrigger, { value: "references", onClick: loadReferences, children: [_jsx(Link2, { className: "h-3.5 w-3.5 mr-1" }), "References", refs && (_jsx(Badge, { variant: "outline", className: "ml-1.5 text-[10px]", children: refs.length }))] })), hasAnchors && (_jsxs(TabsTrigger, { value: "related", children: [_jsx(Layers3, { className: "h-3.5 w-3.5 mr-1" }), "Related"] }))] }), DesignerTab && (_jsx(TabsContent, { value: "designer", className: "mt-4", children: _jsx(DesignerTab, { type: type, name: name }) })), _jsx(TabsContent, { value: "form", className: "mt-4", children: _jsx(SchemaForm, { schema: schema, form: entry?.form, value: draft, onChange: setDraft, issues: issues, hiddenFields: config.hiddenFields, fieldOrder: config.fieldOrder, readOnly: readOnly, createMode: createMode, widgetContext: widgetContext }) }), !createMode && (_jsx(TabsContent, { value: "layers", className: "mt-4", children: _jsx(LayeredDiff, { layered: layered }) })), !createMode && (_jsx(TabsContent, { value: "references", className: "mt-4", children: _jsx(ReferencesPanel, { refs: refs, loading: refsLoading }) })), hasAnchors && (_jsx(TabsContent, { value: "related", className: "mt-4", children: _jsx(RelatedPanel, { type: type, name: name, parentItem: draft, onOpen: (t) => setRelatedTarget(t) }) }))] })] }), _jsx(MetadataDetailDrawer, { target: relatedTarget, onClose: () => setRelatedTarget(null), parentContext: { type, name } }), _jsx(Dialog, { open: destructiveIssues != null, onOpenChange: (open) => {
288
+ if (!open) {
289
+ setDestructiveIssues(null);
290
+ setPendingItem(null);
291
+ }
292
+ }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(AlertTriangle, { className: "h-5 w-5 text-amber-600" }), "Destructive change detected"] }), _jsx(DialogDescription, { children: "The framework refused this save because it would drop or narrow data already in use. Review the issues and confirm to override." })] }), _jsx("div", { className: "max-h-[300px] overflow-auto space-y-2 my-2", children: destructiveIssues?.map((i, idx) => (_jsxs("div", { className: "rounded border bg-amber-50 border-amber-200 p-2 text-xs", children: [_jsx("div", { className: "font-mono text-amber-900", children: i.kind ?? 'change' }), i.path && (_jsx("div", { className: "text-amber-800 font-mono mt-0.5", children: i.path })), _jsx("div", { className: "text-amber-900 mt-1", children: i.message })] }, idx))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => setDestructiveIssues(null), children: "Cancel" }), _jsx(Button, { variant: "destructive", onClick: () => doSave(true), disabled: saving, children: saving ? 'Forcing…' : 'Force save' })] })] }) })] }));
293
+ }
294
+ function ReferencesPanel({ refs, loading, }) {
295
+ if (loading || refs == null) {
296
+ return (_jsxs("div", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Scanning references\u2026"] }));
297
+ }
298
+ if (refs.length === 0) {
299
+ return (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No references found" }), _jsx(EmptyDescription, { children: "Nothing in the metadata graph points at this item. Safe to delete." })] }));
300
+ }
301
+ return (_jsx("div", { className: "border rounded-lg overflow-hidden", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-xs uppercase tracking-wider text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left", children: "From type" }), _jsx("th", { className: "px-3 py-2 text-left", children: "From name" }), _jsx("th", { className: "px-3 py-2 text-left", children: "Path" })] }) }), _jsx("tbody", { className: "divide-y", children: refs.map((r, i) => (_jsxs("tr", { className: "hover:bg-accent/50", children: [_jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: "outline", className: "text-[10px] font-mono", children: r.fromType }) }), _jsx("td", { className: "px-3 py-2 font-mono text-xs", children: r.fromName }), _jsx("td", { className: "px-3 py-2 font-mono text-xs text-muted-foreground", children: r.path })] }, i))) })] }) }));
302
+ }
@@ -0,0 +1,5 @@
1
+ export interface MetadataResourceHistoryPageProps {
2
+ type?: string;
3
+ name?: string;
4
+ }
5
+ export declare function MetadataResourceHistoryPage({ type: typeProp, name: nameProp, }: MetadataResourceHistoryPageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,100 @@
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
+ * MetadataResourceHistoryPage — durable change log for one item
5
+ * (Phase 3c).
6
+ *
7
+ * Calls `client.history(type, name)`. The framework returns
8
+ * `{ events: MetadataEvent[] }` where each event records who saved
9
+ * what, with a monotonic `seq`. We render a vertical timeline; clicking
10
+ * an event expands its payload diff.
11
+ *
12
+ * Rollback is intentionally not exposed in MVP — restoring a previous
13
+ * version is just `client.save(type, name, oldPayload)` which already
14
+ * works, but a one-click revert needs a confirmation flow we'll add
15
+ * once admins ask for it.
16
+ */
17
+ import * as React from 'react';
18
+ import { useNavigate, useParams } from 'react-router-dom';
19
+ import { ArrowLeft, RefreshCw, Loader2 } from 'lucide-react';
20
+ import { Button } from '@object-ui/components';
21
+ import { Badge } from '@object-ui/components';
22
+ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
23
+ import { PageShell } from './PageShell';
24
+ import { useMetadataClient, useMetadataTypes, } from './useMetadata';
25
+ export function MetadataResourceHistoryPage({ type: typeProp, name: nameProp, }) {
26
+ const params = useParams();
27
+ const type = typeProp ?? params.type ?? '';
28
+ const name = nameProp ?? params.name ?? '';
29
+ const navigate = useNavigate();
30
+ const client = useMetadataClient();
31
+ const { entries } = useMetadataTypes(client);
32
+ const entry = entries.find((t) => t.type === type);
33
+ const [events, setEvents] = React.useState([]);
34
+ const [loading, setLoading] = React.useState(true);
35
+ const [error, setError] = React.useState(null);
36
+ const [refreshKey, setRefreshKey] = React.useState(0);
37
+ const [expanded, setExpanded] = React.useState(null);
38
+ React.useEffect(() => {
39
+ let cancelled = false;
40
+ setLoading(true);
41
+ setError(null);
42
+ (async () => {
43
+ try {
44
+ const result = await client.history(type, name, { limit: 100 });
45
+ if (cancelled)
46
+ return;
47
+ const list = Array.isArray(result)
48
+ ? result
49
+ : Array.isArray(result?.events)
50
+ ? result.events
51
+ : [];
52
+ // Reverse chronological — most recent first.
53
+ const sorted = [...list].sort((a, b) => (b.seq ?? 0) - (a.seq ?? 0));
54
+ setEvents(sorted);
55
+ setLoading(false);
56
+ }
57
+ catch (err) {
58
+ if (!cancelled) {
59
+ setError(err?.message ?? String(err));
60
+ setLoading(false);
61
+ }
62
+ }
63
+ })();
64
+ return () => {
65
+ cancelled = true;
66
+ };
67
+ }, [client, type, name, refreshKey]);
68
+ return (_jsx(PageShell, { entry: entry ?? { type, label: type }, itemName: name, subtitle: "Version history", stats: [{ label: 'Events', value: events.length }], actions: _jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`../${encodeURIComponent(name)}`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Back to item"] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setRefreshKey((k) => k + 1), children: _jsx(RefreshCw, { className: "h-4 w-4" }) })] }), children: _jsxs("div", { className: "p-6 max-w-4xl", children: [loading && (_jsxs("div", { className: "text-sm text-muted-foreground flex items-center gap-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), " Loading history\u2026"] })), error && (_jsx("div", { className: "text-sm text-destructive border border-destructive/30 rounded p-3 bg-destructive/5", children: error })), !loading && !error && events.length === 0 && (_jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "No history yet" }), _jsx(EmptyDescription, { children: "This item has never been edited via an overlay. The first save will create the initial history record." })] })), !loading && events.length > 0 && (_jsx("ol", { className: "space-y-2 relative pl-6 border-l", children: events.map((ev, i) => {
69
+ const isOpen = expanded === i;
70
+ return (_jsxs("li", { className: "relative", children: [_jsx("span", { className: "absolute -left-[27px] top-2 w-3 h-3 rounded-full bg-primary ring-4 ring-background" }), _jsxs("div", { role: "button", tabIndex: 0, onClick: () => setExpanded(isOpen ? null : i), onKeyDown: (e) => {
71
+ if (e.key === 'Enter' || e.key === ' ') {
72
+ setExpanded(isOpen ? null : i);
73
+ }
74
+ }, className: "border rounded-lg p-3 hover:border-primary/50 cursor-pointer transition-colors", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsxs(Badge, { variant: "outline", className: "text-[10px] font-mono", children: ["seq ", ev.seq ?? '–'] }), ev.kind && (_jsx(Badge, { className: 'text-[10px] ' +
75
+ (ev.kind === 'delete' || ev.kind === 'tombstone'
76
+ ? 'bg-red-100 text-red-800 hover:bg-red-100'
77
+ : ev.kind === 'create'
78
+ ? 'bg-blue-100 text-blue-800 hover:bg-blue-100'
79
+ : 'bg-emerald-100 text-emerald-800 hover:bg-emerald-100'), children: String(ev.kind) })), ev.actor && (_jsxs("span", { className: "text-xs text-muted-foreground", children: ["by", ' ', _jsx("span", { className: "font-mono", children: String(ev.actor) })] })), ev.at && (_jsx("span", { className: "text-xs text-muted-foreground ml-auto", children: formatWhen(ev.at) }))] }), isOpen && (_jsx("pre", { className: "mt-2 text-xs font-mono bg-muted/30 rounded p-2 overflow-auto max-h-[280px]", children: safeStringify(ev) }))] })] }, `${ev.seq ?? i}-${i}`));
80
+ }) }))] }) }));
81
+ }
82
+ function formatWhen(at) {
83
+ try {
84
+ const d = typeof at === 'number' ? new Date(at) : new Date(at);
85
+ if (Number.isNaN(d.getTime()))
86
+ return String(at);
87
+ return d.toLocaleString();
88
+ }
89
+ catch {
90
+ return String(at);
91
+ }
92
+ }
93
+ function safeStringify(v) {
94
+ try {
95
+ return JSON.stringify(v, null, 2);
96
+ }
97
+ catch {
98
+ return String(v);
99
+ }
100
+ }
@@ -0,0 +1,4 @@
1
+ export interface MetadataResourceListPageProps {
2
+ type?: string;
3
+ }
4
+ export declare function MetadataResourceListPage({ type: typeProp }: MetadataResourceListPageProps): import("react/jsx-runtime").JSX.Element;