@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,169 @@
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
+ * Built-in component registrations — Phase 3b + 3c.
5
+ *
6
+ * Side-effect module imported by `index.ts` to ensure the platform's
7
+ * own admin UI is registered with the ComponentRegistry before
8
+ * AppContent mounts the first `<Route path="component/...">`.
9
+ *
10
+ * Plugins follow the same pattern: their package entry point imports
11
+ * a similar registration module, so `import '@object-ui/plugin-foo'`
12
+ * is enough to make the plugin's component refs reachable from app
13
+ * metadata.
14
+ *
15
+ * Phase 3c — also registers the new generic metadata admin engine and
16
+ * pre-wires specialised editors for `object` and `field` so admins
17
+ * still get the polished ObjectManager / FieldDesigner experience
18
+ * inside the unified Setup-app shell.
19
+ */
20
+ import { lazy, Suspense } from 'react';
21
+ import { registerAppComponent } from './componentRegistry';
22
+ import { MetadataDirectoryPage, MetadataResourceRouter, registerMetadataResource, } from '../views/metadata-admin';
23
+ import { PermissionMatrixEditPage } from '../views/metadata-admin/PermissionMatrixEditor';
24
+ import { DesignerEditorBody } from '../views/metadata-admin/DesignerEditorWrapper';
25
+ /* -------------------------------------------------------------------------- */
26
+ /* 1) Top-level admin pages — bound to `metadata:directory` + `metadata:resource` */
27
+ /* -------------------------------------------------------------------------- */
28
+ registerAppComponent({
29
+ ref: 'metadata:directory',
30
+ label: 'All Metadata Types',
31
+ source: '@object-ui/app-shell',
32
+ component: MetadataDirectoryPage,
33
+ });
34
+ registerAppComponent({
35
+ ref: 'metadata:resource',
36
+ label: 'Metadata Resource',
37
+ source: '@object-ui/app-shell',
38
+ // The router switches between list / new / edit / history based on
39
+ // the sub-path under `/component/metadata/resource/...`.
40
+ component: MetadataResourceRouter,
41
+ });
42
+ /* -------------------------------------------------------------------------- */
43
+ /* 2) Specialised editors for flagship types — opt the generic engine */
44
+ /* out for the types that already have polished bespoke surfaces. */
45
+ /* */
46
+ /* Note: `object` and `field` intentionally use the generic engine so the */
47
+ /* Metadata Directory has a consistent list/edit experience across every */
48
+ /* type (only `permission`, `view`, `dashboard`, `page` keep bespoke */
49
+ /* editors below — those are visual designers, not list/form pages). */
50
+ /* -------------------------------------------------------------------------- */
51
+ registerMetadataResource({
52
+ type: 'object',
53
+ label: 'Objects',
54
+ description: 'Domain entities — tables in the data model. Each object owns its fields, relationships, validations, and lifecycle hooks.',
55
+ domain: 'data',
56
+ searchableFields: ['name', 'label', 'description'],
57
+ listColumns: [
58
+ { key: 'name', label: 'Name', width: '25%' },
59
+ { key: 'label', label: 'Label', width: '25%' },
60
+ { key: 'description', label: 'Description' },
61
+ ],
62
+ });
63
+ registerMetadataResource({
64
+ type: 'field',
65
+ label: 'Fields',
66
+ description: 'Columns attached to objects — name, type, validation, and storage settings.',
67
+ domain: 'data',
68
+ searchableFields: ['name', 'label', 'object', 'type'],
69
+ listColumns: [
70
+ { key: 'name', label: 'Name', width: '25%' },
71
+ { key: 'object', label: 'Object', width: '20%' },
72
+ { key: 'type', label: 'Type', width: '15%' },
73
+ { key: 'label', label: 'Label' },
74
+ ],
75
+ });
76
+ /* -------------------------------------------------------------------------- */
77
+ /* 3) Permission matrix editor — replaces the generic AutoForm for */
78
+ /* `type=permission` with a Salesforce-style grid (Phase 3e). */
79
+ /* -------------------------------------------------------------------------- */
80
+ registerMetadataResource({
81
+ type: 'permission',
82
+ label: 'Permission sets',
83
+ description: 'Object-level CRUD + VAMA + lifecycle permissions, and field-level R/W. Profiles are permission sets with isProfile=true.',
84
+ domain: 'security',
85
+ EditPage: PermissionMatrixEditPage,
86
+ searchableFields: ['name', 'label'],
87
+ listColumns: [
88
+ { key: 'name', label: 'Name', width: '30%' },
89
+ { key: 'label', label: 'Label', width: '30%' },
90
+ {
91
+ key: 'isProfile',
92
+ label: 'Type',
93
+ width: '15%',
94
+ render: (v) => (v ? 'Profile' : 'Permission set'),
95
+ },
96
+ ],
97
+ });
98
+ /* -------------------------------------------------------------------------- */
99
+ /* 4) Bespoke designers wired through DesignerEditorWrapper (Phase 3d). */
100
+ /* These types continue to use the generic AutoForm-style list and */
101
+ /* history pages but swap the edit page for the existing visual designer. */
102
+ /* -------------------------------------------------------------------------- */
103
+ const ObjectViewConfigurator = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.ObjectViewConfigurator })));
104
+ const DashboardEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.DashboardEditor })));
105
+ const PageCanvasEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.PageCanvasEditor })));
106
+ function ViewEditPage(props) {
107
+ return (_jsx(DesignerEditorBody, { ...props, fromMetadata: (raw) => {
108
+ // Normalize backend view metadata into the shape ObjectViewConfigurator expects.
109
+ // Built-in views may omit `columns` or use legacy field names.
110
+ const base = raw && typeof raw === 'object' ? raw : {};
111
+ return {
112
+ viewType: base.viewType ?? base.type ?? 'grid',
113
+ columns: Array.isArray(base.columns) ? base.columns : [],
114
+ filters: Array.isArray(base.filters) ? base.filters : [],
115
+ sorts: Array.isArray(base.sorts) ? base.sorts : [],
116
+ pageSize: typeof base.pageSize === 'number' ? base.pageSize : 25,
117
+ ...base,
118
+ };
119
+ }, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "view designer" }), children: _jsx(ObjectViewConfigurator, { config: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
120
+ }
121
+ function DashboardEditPage(props) {
122
+ return (_jsx(DesignerEditorBody, { ...props, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "dashboard editor" }), children: _jsx(DashboardEditor, { schema: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
123
+ }
124
+ function PageEditPage(props) {
125
+ return (_jsx(DesignerEditorBody, { ...props, renderDesigner: (value, onChange, readOnly) => (_jsx(Suspense, { fallback: _jsx(DesignerFallback, { label: "page canvas" }), children: _jsx(PageCanvasEditor, { schema: value, onChange: (next) => onChange(next), readOnly: readOnly }) })) }));
126
+ }
127
+ function DesignerFallback({ label }) {
128
+ return (_jsxs("div", { className: "p-6 text-sm text-muted-foreground", children: ["Loading ", label, "\u2026"] }));
129
+ }
130
+ registerMetadataResource({
131
+ type: 'view',
132
+ label: 'Views',
133
+ description: 'Saved list / kanban / calendar / gantt configurations on top of an object.',
134
+ domain: 'ui',
135
+ DesignerTab: ViewEditPage,
136
+ designerTabLabel: 'View designer',
137
+ listColumns: [
138
+ { key: 'name', label: 'Name', width: '30%' },
139
+ { key: 'object', label: 'Object', width: '20%' },
140
+ { key: 'type', label: 'Type', width: '15%' },
141
+ { key: 'label', label: 'Label' },
142
+ ],
143
+ });
144
+ registerMetadataResource({
145
+ type: 'dashboard',
146
+ label: 'Dashboards',
147
+ description: 'Composed dashboards with charts, KPIs, and tables.',
148
+ domain: 'ui',
149
+ DesignerTab: DashboardEditPage,
150
+ designerTabLabel: 'Dashboard designer',
151
+ listColumns: [
152
+ { key: 'name', label: 'Name', width: '30%' },
153
+ { key: 'label', label: 'Label', width: '30%' },
154
+ { key: 'description', label: 'Description' },
155
+ ],
156
+ });
157
+ registerMetadataResource({
158
+ type: 'page',
159
+ label: 'Pages',
160
+ description: 'Visual page layouts authored in the Page Canvas editor.',
161
+ domain: 'ui',
162
+ DesignerTab: PageEditPage,
163
+ designerTabLabel: 'Page canvas',
164
+ listColumns: [
165
+ { key: 'name', label: 'Name', width: '30%' },
166
+ { key: 'label', label: 'Label', width: '30%' },
167
+ { key: 'route', label: 'Route' },
168
+ ],
169
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ComponentRegistry — central lookup for first-party UI components that
3
+ * are addressable from App metadata via the `component` nav-item type.
4
+ *
5
+ * Phase 3b: introduced so the framework's Setup app (and any other app)
6
+ * can declare admin/setup surfaces declaratively:
7
+ *
8
+ * ```ts
9
+ * { id: 'nav_objects', type: 'component',
10
+ * componentRef: 'metadata:resource',
11
+ * params: { type: 'object' } }
12
+ * ```
13
+ *
14
+ * Why a registry instead of importing the component directly in
15
+ * AppContent.tsx?
16
+ * • Keeps the app-shell agnostic of which plugins are installed.
17
+ * • Lets plugin packages (plugin-designer, plugin-permissions,
18
+ * plugin-metadata-admin, …) register their pages without
19
+ * forcing a coupled import graph.
20
+ * • Provides a single place to render a friendly "component not
21
+ * registered" empty state when a metadata entry references a
22
+ * plugin that isn't loaded.
23
+ *
24
+ * Convention: registry keys are `namespace:name` (colon-separated).
25
+ * The namespace maps to a route segment so URLs stay clean:
26
+ * `metadata:resource` → `/component/metadata/resource`
27
+ * Params from the nav metadata are serialised as query string.
28
+ */
29
+ import type { ComponentType } from 'react';
30
+ export type AppComponentRegistryEntry = {
31
+ /** Registry key, e.g. `metadata:resource`. */
32
+ ref: string;
33
+ /** Human-readable label for diagnostics / "Component not found" empty state. */
34
+ label?: string;
35
+ /** Owning plugin / package id, for diagnostics. */
36
+ source?: string;
37
+ /** The React component. Receives the merged params (nav `params` + URL query) as props. */
38
+ component: ComponentType<any>;
39
+ };
40
+ /**
41
+ * Register (or replace) a component. Idempotent — re-registering with the
42
+ * same `ref` overwrites the previous entry, which is the expected behavior
43
+ * during HMR / dev workflows.
44
+ */
45
+ export declare function registerAppComponent(entry: AppComponentRegistryEntry): void;
46
+ /**
47
+ * Look up a component by ref. Returns `undefined` if not registered;
48
+ * AppContent surfaces this as a structured empty state so the operator
49
+ * knows which plugin is missing.
50
+ */
51
+ export declare function getAppComponent(ref: string): AppComponentRegistryEntry | undefined;
52
+ /**
53
+ * Snapshot of all registered components — used by diagnostics surfaces
54
+ * (e.g. a future "Installed UI Components" admin page).
55
+ */
56
+ export declare function listAppComponents(): AppComponentRegistryEntry[];
57
+ /**
58
+ * Convert `metadata:resource` ↔ URL segments `['metadata', 'resource']`.
59
+ * Component refs are restricted to one colon in MVP, but the helper is
60
+ * future-proofed for nested keys (`metadata:resource:edit`).
61
+ */
62
+ export declare function componentRefToUrlSegments(ref: string): string[];
63
+ export declare function urlSegmentsToComponentRef(segments: string[]): string;
@@ -0,0 +1,36 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ const REGISTRY = new Map();
3
+ /**
4
+ * Register (or replace) a component. Idempotent — re-registering with the
5
+ * same `ref` overwrites the previous entry, which is the expected behavior
6
+ * during HMR / dev workflows.
7
+ */
8
+ export function registerAppComponent(entry) {
9
+ REGISTRY.set(entry.ref, entry);
10
+ }
11
+ /**
12
+ * Look up a component by ref. Returns `undefined` if not registered;
13
+ * AppContent surfaces this as a structured empty state so the operator
14
+ * knows which plugin is missing.
15
+ */
16
+ export function getAppComponent(ref) {
17
+ return REGISTRY.get(ref);
18
+ }
19
+ /**
20
+ * Snapshot of all registered components — used by diagnostics surfaces
21
+ * (e.g. a future "Installed UI Components" admin page).
22
+ */
23
+ export function listAppComponents() {
24
+ return Array.from(REGISTRY.values());
25
+ }
26
+ /**
27
+ * Convert `metadata:resource` ↔ URL segments `['metadata', 'resource']`.
28
+ * Component refs are restricted to one colon in MVP, but the helper is
29
+ * future-proofed for nested keys (`metadata:resource:edit`).
30
+ */
31
+ export function componentRefToUrlSegments(ref) {
32
+ return ref.split(':').filter(Boolean);
33
+ }
34
+ export function urlSegmentsToComponentRef(segments) {
35
+ return segments.filter(Boolean).join(':');
36
+ }
@@ -0,0 +1,6 @@
1
+ export interface ComponentNavViewProps {
2
+ /** Extra props injected by the parent route element (typically empty). */
3
+ extraProps?: Record<string, unknown>;
4
+ }
5
+ export declare function ComponentNavView({ extraProps }?: ComponentNavViewProps): import("react/jsx-runtime").JSX.Element;
6
+ export default ComponentNavView;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useParams, useSearchParams } from 'react-router-dom';
3
+ import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
4
+ import { getAppComponent } from '../services/componentRegistry';
5
+ export function ComponentNavView({ extraProps } = {}) {
6
+ const params = useParams();
7
+ const [search] = useSearchParams();
8
+ // URL: /apps/:appName/component/:ns/:name (sub-routes via /*)
9
+ const ns = params.ns;
10
+ const name = params.name;
11
+ const ref = ns && name ? `${ns}:${name}` : (ns ?? '');
12
+ const entry = getAppComponent(ref);
13
+ if (!entry) {
14
+ return (_jsx("div", { className: "h-full flex items-center justify-center p-8", children: _jsxs(Empty, { children: [_jsx(EmptyTitle, { children: "Component not registered" }), _jsxs(EmptyDescription, { children: ["No component is registered for ", _jsx("code", { className: "font-mono", children: ref || '(empty)' }), ". Ensure the plugin that provides this surface is installed and has called", _jsx("code", { className: "font-mono", children: " registerAppComponent()" }), "."] })] }) }));
15
+ }
16
+ // Merge query string into props. Strings only at this layer — the
17
+ // component can coerce as needed. Nav-metadata `params` (the
18
+ // `{ type: 'object' }` from setup.app.ts) are forwarded by the
19
+ // sidebar URL builder as query string, so this merge captures them.
20
+ const queryProps = {};
21
+ for (const [k, v] of search.entries())
22
+ queryProps[k] = v;
23
+ const Component = entry.component;
24
+ return _jsx(Component, { ...queryProps, ...(extraProps ?? {}) });
25
+ }
26
+ export default ComponentNavView;
@@ -370,6 +370,24 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
370
370
  const params = (action.params && !Array.isArray(action.params))
371
371
  ? action.params
372
372
  : {};
373
+ // ── Popup-blocker workaround ──────────────────────────────────────
374
+ // When `action.opensInNewTab` is set, the handler is known to return
375
+ // `{ redirectUrl: ... }` for the UI to navigate to. We pre-open
376
+ // `about:blank` synchronously *here*, before the await fetch — this
377
+ // preserves the user-gesture context so Chrome/Safari don't block
378
+ // the eventual navigation. Drives the same tab to `redirectUrl`
379
+ // after the server replies. If pre-open fails (popup blocker on the
380
+ // initial gesture), we fall back to navigating the current tab so
381
+ // the user always gets there.
382
+ let preOpenedTab = null;
383
+ if (action.opensInNewTab) {
384
+ try {
385
+ preOpenedTab = window.open('about:blank', '_blank', 'noopener,noreferrer');
386
+ }
387
+ catch {
388
+ preOpenedTab = null;
389
+ }
390
+ }
373
391
  try {
374
392
  const baseUrl = import.meta.env.VITE_SERVER_URL || '';
375
393
  const obj = action.objectName || objectName || 'global';
@@ -381,6 +399,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
381
399
  const json = await res.json().catch(() => null);
382
400
  if (!res.ok || (json && json.success === false)) {
383
401
  const errMsg = json?.error || `Action "${targetName}" failed (HTTP ${res.status})`;
402
+ if (preOpenedTab) {
403
+ try {
404
+ preOpenedTab.close();
405
+ }
406
+ catch { /* ignore */ }
407
+ }
384
408
  return { success: false, error: errMsg };
385
409
  }
386
410
  const shouldRefresh = action.refreshAfter !== false;
@@ -389,19 +413,55 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
389
413
  const result = json?.data;
390
414
  // ── redirectUrl convention ────────────────────────────────────────
391
415
  // A script-action handler can return `{ redirectUrl: 'https://…' }`
392
- // to ask the UI to open the URL in a new tab. Used by the
393
- // `sso_as_owner` action on sys_environment to drop the operator
394
- // into the env runtime as its admin. Same pattern works for any
395
- // future "deep link" style action.
416
+ // to ask the UI to open the URL. If the action declared
417
+ // `opensInNewTab: true`, we drive the pre-opened tab to that URL
418
+ // (popup-blocker-safe). Otherwise we open lazily and, if blocked,
419
+ // fall back to navigating the current tab so the user always gets
420
+ // to the destination.
396
421
  if (result && typeof result === 'object' && typeof result.redirectUrl === 'string') {
422
+ const redirectUrl = result.redirectUrl;
423
+ if (preOpenedTab) {
424
+ try {
425
+ preOpenedTab.location.href = redirectUrl;
426
+ }
427
+ catch {
428
+ try {
429
+ preOpenedTab.close();
430
+ }
431
+ catch { /* ignore */ }
432
+ window.location.href = redirectUrl;
433
+ }
434
+ }
435
+ else {
436
+ let popup = null;
437
+ try {
438
+ popup = window.open(redirectUrl, '_blank', 'noopener,noreferrer');
439
+ }
440
+ catch {
441
+ popup = null;
442
+ }
443
+ if (!popup) {
444
+ window.location.href = redirectUrl;
445
+ }
446
+ }
447
+ }
448
+ else if (preOpenedTab) {
449
+ // Handler didn't return a redirectUrl — close the empty tab we
450
+ // optimistically pre-opened so the user isn't left with about:blank.
397
451
  try {
398
- window.open(result.redirectUrl, '_blank', 'noopener,noreferrer');
452
+ preOpenedTab.close();
399
453
  }
400
- catch { /* popup blocked — fall through */ }
454
+ catch { /* ignore */ }
401
455
  }
402
456
  return { success: true, data: result, reload: shouldRefresh };
403
457
  }
404
458
  catch (error) {
459
+ if (preOpenedTab) {
460
+ try {
461
+ preOpenedTab.close();
462
+ }
463
+ catch { /* ignore */ }
464
+ }
405
465
  return { success: false, error: error.message };
406
466
  }
407
467
  }, [authFetch, pureRecordId, objectName]);
@@ -28,7 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
28
28
  *
29
29
  * @module views/RecordFormPage
30
30
  */
31
- import { useCallback, useMemo } from 'react';
31
+ import { useCallback, useEffect, useMemo, useState } from 'react';
32
32
  import { useParams, useNavigate, useSearchParams, Link } from 'react-router-dom';
33
33
  import { ObjectForm } from '@object-ui/plugin-form';
34
34
  import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
@@ -58,7 +58,19 @@ export function RecordFormPage({ mode }) {
58
58
  const { objects, loading: metadataLoading } = useMetadata();
59
59
  const { t } = useObjectTranslation();
60
60
  const { objectLabel } = useObjectLabel();
61
- const { user } = useAuth();
61
+ const { user, getAuthConfig } = useAuth();
62
+ // Pull deployment-level feature flags so action visibility predicates
63
+ // (e.g. `features.multiOrgEnabled != false` on sys_organization's create
64
+ // action) can see them inside the nested ExpressionProvider below.
65
+ const [features, setFeatures] = useState({});
66
+ useEffect(() => {
67
+ let cancelled = false;
68
+ getAuthConfig()
69
+ .then(cfg => { if (!cancelled)
70
+ setFeatures(cfg?.features ?? {}); })
71
+ .catch(() => { });
72
+ return () => { cancelled = true; };
73
+ }, [getAuthConfig]);
62
74
  /**
63
75
  * Query-string prefills for create mode. Used by related-list "+ New"
64
76
  * buttons that pass the parent record id as a `<referenceField>=<id>`
@@ -162,7 +174,7 @@ export function RecordFormPage({ mode }) {
162
174
  if (!objectDef) {
163
175
  return (_jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) }), _jsx("div", { className: "mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => navigate(baseUrl), children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), t('empty.back')] }) })] }) }));
164
176
  }
165
- return (_jsx(ExpressionProvider, { user: expressionUser, app: { name: appName }, data: {}, children: _jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", "data-testid": "record-form-page", "data-mode": mode, children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-3 border-b bg-background px-4 py-3 sm:px-6", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: goBack, "data-testid": "record-form-page-back", "aria-label": t('common.back', { defaultValue: 'Back' }), children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Link, { to: objectListUrl, className: "hover:text-foreground transition-colors", children: label }), _jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("span", { className: "text-foreground font-medium", "data-testid": "record-form-page-title", children: pageTitle }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy, className: "ml-1" })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto max-w-4xl", children: _jsx(ObjectForm, { schema: {
177
+ return (_jsx(ExpressionProvider, { user: expressionUser, app: { name: appName }, data: {}, features: features, children: _jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", "data-testid": "record-form-page", "data-mode": mode, children: [_jsxs("header", { className: "sticky top-0 z-10 flex items-center gap-3 border-b bg-background px-4 py-3 sm:px-6", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: goBack, "data-testid": "record-form-page-back", "aria-label": t('common.back', { defaultValue: 'Back' }), children: _jsx(ArrowLeft, { className: "h-4 w-4" }) }), _jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Link, { to: objectListUrl, className: "hover:text-foreground transition-colors", children: label }), _jsx("span", { "aria-hidden": "true", children: "/" }), _jsx("span", { className: "text-foreground font-medium", "data-testid": "record-form-page-title", children: pageTitle }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy, className: "ml-1" })] })] }), _jsx("div", { className: "flex-1 overflow-auto p-4 sm:p-6", children: _jsx("div", { className: "mx-auto max-w-4xl", children: _jsx(ObjectForm, { schema: {
166
178
  type: 'object-form',
167
179
  formType: 'simple',
168
180
  objectName: objectDef.name,
@@ -66,6 +66,10 @@ export function SearchResultsPage() {
66
66
  href = `${baseUrl}/page/${item.pageName}`;
67
67
  else if (item.type === 'report')
68
68
  href = `${baseUrl}/report/${item.reportName}`;
69
+ else if (item.type === 'component' && item.componentRef) {
70
+ const segs = String(item.componentRef).split(':').filter(Boolean);
71
+ href = `${baseUrl}/component/${segs.join('/')}`;
72
+ }
69
73
  return {
70
74
  id: item.id,
71
75
  label: item.label || item.objectName || item.dashboardName || item.pageName || item.reportName || '',
@@ -0,0 +1,68 @@
1
+ /**
2
+ * DesignerEditorWrapper — Phase 3d.
3
+ *
4
+ * Generic "load metadata → hand to a controlled designer component →
5
+ * save on commit" wrapper. Lets us plug existing bespoke designers
6
+ * (ObjectViewConfigurator, DashboardEditor, PageDesigner, …) into the
7
+ * unified Setup-app shell without rewriting them.
8
+ *
9
+ * Each designer takes a different prop shape, so we accept a
10
+ * `renderDesigner` callback that gets `(value, onChange, readOnly)`
11
+ * and returns whatever the designer needs.
12
+ *
13
+ * Wiring is dead simple — see `builtinComponents.tsx`:
14
+ *
15
+ * registerMetadataResource({
16
+ * type: 'view',
17
+ * EditPage: (props) => (
18
+ * <DesignerEditorWrapper
19
+ * {...props}
20
+ * renderDesigner={(value, onChange, readOnly) => (
21
+ * <ObjectViewConfigurator config={value} onChange={onChange} readOnly={readOnly} />
22
+ * )}
23
+ * />
24
+ * ),
25
+ * });
26
+ *
27
+ * The wrapper handles:
28
+ * • Initial fetch via `client.layered()` (so admins see overlay vs code).
29
+ * • Local edit state with Save / Revert.
30
+ * • Destructive-change confirmation (409 → dialog → retry with force).
31
+ * • Reset overlay button (DELETE).
32
+ * • Read-only fallback when the type isn't allowed to override at runtime
33
+ * (driven by /meta/types#allowOrgOverride).
34
+ *
35
+ * Validation errors (422) are surfaced as an error banner; bespoke
36
+ * designers usually handle their own field-level UX.
37
+ */
38
+ import * as React from 'react';
39
+ export interface DesignerEditorWrapperProps<TValue = any> {
40
+ type: string;
41
+ name: string;
42
+ /**
43
+ * Render the actual designer. Receives the current draft value, a
44
+ * setter, and whether the editor must be read-only.
45
+ */
46
+ renderDesigner: (value: TValue, onChange: (next: TValue) => void, readOnly: boolean) => React.ReactNode;
47
+ /**
48
+ * Optional adapter to normalise the value the server returns into
49
+ * the shape the designer wants. Defaults to identity.
50
+ */
51
+ fromMetadata?: (raw: unknown) => TValue;
52
+ /**
53
+ * Optional adapter to turn the designer's value back into the
54
+ * metadata payload before save. Defaults to identity.
55
+ */
56
+ toMetadata?: (value: TValue) => unknown;
57
+ }
58
+ export declare function DesignerEditorWrapper<TValue = any>(props: DesignerEditorWrapperProps<TValue>): import("react/jsx-runtime").JSX.Element;
59
+ /**
60
+ * Embedded variant — same state machine, but no surrounding `PageShell`.
61
+ * Used by `ResourceEditPage` to host the designer inside a tab alongside
62
+ * the generic Form / Layers / References tabs. The action toolbar (Save /
63
+ * Reset / Refresh) is rendered inline at the top of the panel so the tab
64
+ * remains self-sufficient.
65
+ */
66
+ export declare function DesignerEditorBody<TValue = any>({ type, name, renderDesigner, fromMetadata, toMetadata, withChrome, }: DesignerEditorWrapperProps<TValue> & {
67
+ withChrome?: boolean;
68
+ }): import("react/jsx-runtime").JSX.Element;