@object-ui/app-shell 6.0.4 → 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.
Files changed (81) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +10 -1
  3. package/dist/console/AppContent.js +24 -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 +83 -17
  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 +166 -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 +72 -0
  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 +58 -0
  46. package/dist/views/metadata-admin/DesignerEditorWrapper.js +140 -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/PageShell.d.ts +34 -0
  52. package/dist/views/metadata-admin/PageShell.js +33 -0
  53. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
  54. package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
  55. package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
  56. package/dist/views/metadata-admin/QuickFind.js +152 -0
  57. package/dist/views/metadata-admin/ResourceEditPage.d.ts +7 -0
  58. package/dist/views/metadata-admin/ResourceEditPage.js +256 -0
  59. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
  60. package/dist/views/metadata-admin/ResourceHistoryPage.js +97 -0
  61. package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
  62. package/dist/views/metadata-admin/ResourceListPage.js +144 -0
  63. package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
  64. package/dist/views/metadata-admin/ResourceRouter.js +47 -0
  65. package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
  66. package/dist/views/metadata-admin/SchemaForm.js +556 -0
  67. package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
  68. package/dist/views/metadata-admin/default-schemas.js +207 -0
  69. package/dist/views/metadata-admin/i18n.d.ts +33 -0
  70. package/dist/views/metadata-admin/i18n.js +303 -0
  71. package/dist/views/metadata-admin/index.d.ts +31 -0
  72. package/dist/views/metadata-admin/index.js +33 -0
  73. package/dist/views/metadata-admin/predicate.d.ts +31 -0
  74. package/dist/views/metadata-admin/predicate.js +150 -0
  75. package/dist/views/metadata-admin/registry.d.ts +125 -0
  76. package/dist/views/metadata-admin/registry.js +48 -0
  77. package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
  78. package/dist/views/metadata-admin/useMetadata.js +96 -0
  79. package/dist/views/metadata-admin/widgets.d.ts +68 -0
  80. package/dist/views/metadata-admin/widgets.js +287 -0
  81. package/package.json +29 -28
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export interface PermissionMatrixEditPageProps {
2
+ type: string;
3
+ name: string;
4
+ }
5
+ export declare function PermissionMatrixEditPage({ type, name }: PermissionMatrixEditPageProps): import("react/jsx-runtime").JSX.Element;
@@ -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,5 @@
1
+ export interface MetadataQuickFindProps {
2
+ /** Optional app slug for routing; defaults to "setup" if unknown. */
3
+ appSlug?: string;
4
+ }
5
+ export declare function MetadataQuickFind({ appSlug }?: MetadataQuickFindProps): import("react/jsx-runtime").JSX.Element;