@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.
- package/CHANGELOG.md +129 -0
- package/README.md +10 -1
- package/dist/console/AppContent.js +53 -2
- package/dist/console/ai/AiChatPage.d.ts +8 -0
- package/dist/console/ai/AiChatPage.js +188 -0
- package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
- package/dist/console/ai/ConversationsSidebar.js +111 -0
- package/dist/console/auth/LoginPage.js +19 -2
- package/dist/console/auth/RegisterPage.js +30 -1
- package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
- package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
- package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
- package/dist/console/marketplace/MarketplacePage.js +55 -18
- package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
- package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
- package/dist/console/marketplace/usePackageL10n.js +110 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
- package/dist/console/organizations/OrganizationsPage.js +24 -3
- package/dist/context/FavoritesProvider.d.ts +40 -2
- package/dist/context/FavoritesProvider.js +201 -20
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChatConversation.d.ts +7 -0
- package/dist/hooks/useChatConversation.js +37 -5
- package/dist/hooks/useConversationList.d.ts +25 -0
- package/dist/hooks/useConversationList.js +131 -0
- package/dist/hooks/useNavPins.d.ts +11 -4
- package/dist/hooks/useNavPins.js +104 -53
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/layout/AppHeader.js +2 -2
- package/dist/layout/AppSidebar.js +20 -1
- package/dist/layout/UnifiedSidebar.js +1 -1
- package/dist/providers/ExpressionProvider.d.ts +11 -1
- package/dist/providers/ExpressionProvider.js +11 -6
- package/dist/services/builtinComponents.d.ts +1 -0
- package/dist/services/builtinComponents.js +169 -0
- package/dist/services/componentRegistry.d.ts +63 -0
- package/dist/services/componentRegistry.js +36 -0
- package/dist/views/ComponentNavView.d.ts +6 -0
- package/dist/views/ComponentNavView.js +26 -0
- package/dist/views/RecordDetailView.js +66 -6
- package/dist/views/RecordFormPage.js +15 -3
- package/dist/views/SearchResultsPage.js +4 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +68 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +158 -0
- package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
- package/dist/views/metadata-admin/DirectoryPage.js +135 -0
- package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
- package/dist/views/metadata-admin/LayeredDiff.js +26 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.js +52 -0
- package/dist/views/metadata-admin/PageShell.d.ts +34 -0
- package/dist/views/metadata-admin/PageShell.js +40 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
- package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
- package/dist/views/metadata-admin/QuickFind.js +152 -0
- package/dist/views/metadata-admin/RelatedPanel.d.ts +33 -0
- package/dist/views/metadata-admin/RelatedPanel.js +171 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +13 -0
- package/dist/views/metadata-admin/ResourceEditPage.js +302 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.js +100 -0
- package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
- package/dist/views/metadata-admin/ResourceListPage.js +146 -0
- package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
- package/dist/views/metadata-admin/ResourceRouter.js +47 -0
- package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
- package/dist/views/metadata-admin/SchemaForm.js +565 -0
- package/dist/views/metadata-admin/anchors.d.ts +1 -0
- package/dist/views/metadata-admin/anchors.js +229 -0
- package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
- package/dist/views/metadata-admin/default-schemas.js +207 -0
- package/dist/views/metadata-admin/i18n.d.ts +33 -0
- package/dist/views/metadata-admin/i18n.js +303 -0
- package/dist/views/metadata-admin/index.d.ts +33 -0
- package/dist/views/metadata-admin/index.js +39 -0
- package/dist/views/metadata-admin/predicate.d.ts +31 -0
- package/dist/views/metadata-admin/predicate.js +150 -0
- package/dist/views/metadata-admin/registry.d.ts +232 -0
- package/dist/views/metadata-admin/registry.js +106 -0
- package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
- package/dist/views/metadata-admin/useMetadata.js +96 -0
- package/dist/views/metadata-admin/widgets.d.ts +68 -0
- package/dist/views/metadata-admin/widgets.js +287 -0
- package/package.json +27 -26
|
@@ -0,0 +1,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
|
|
393
|
-
// `
|
|
394
|
-
//
|
|
395
|
-
//
|
|
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
|
-
|
|
452
|
+
preOpenedTab.close();
|
|
399
453
|
}
|
|
400
|
-
catch { /*
|
|
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;
|