@object-ui/react 3.3.0 → 3.3.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 +9 -0
- package/README.md +21 -1
- package/dist/context/AppShellContext.d.ts +26 -0
- package/dist/context/AppShellContext.js +67 -0
- package/dist/context/index.d.ts +1 -7
- package/dist/context/index.js +1 -7
- package/package.json +32 -9
- package/.turbo/turbo-build.log +0 -4
- package/src/LazyPluginLoader.tsx +0 -174
- package/src/SchemaRenderer.tsx +0 -266
- package/src/__tests__/LazyPluginLoader.test.tsx +0 -176
- package/src/__tests__/SchemaRenderer.aria.test.tsx +0 -118
- package/src/__tests__/SchemaRenderer.debug.test.tsx +0 -69
- package/src/__tests__/SchemaRenderer.errorBoundary.test.tsx +0 -114
- package/src/__tests__/SchemaRenderer.expressions.test.tsx +0 -131
- package/src/__tests__/SchemaRendererProvider.smoke.test.tsx +0 -132
- package/src/__tests__/ThemeProvider.test.tsx +0 -451
- package/src/__tests__/useNavigationOverlay.test.ts +0 -578
- package/src/__tests__/useOffline.test.ts +0 -198
- package/src/__tests__/usePageTransition.test.ts +0 -214
- package/src/__tests__/usePageVariables.test.tsx +0 -334
- package/src/__tests__/usePerformance.test.ts +0 -137
- package/src/__tests__/useViewData.test.tsx +0 -422
- package/src/__tests__/view-switching.test.tsx +0 -456
- package/src/components/form/FieldFactory.tsx +0 -516
- package/src/components/form/FormRenderer.test.tsx +0 -744
- package/src/components/form/FormRenderer.tsx +0 -258
- package/src/components/form/README.md +0 -352
- package/src/components/form/index.ts +0 -12
- package/src/context/ActionContext.tsx +0 -213
- package/src/context/DndContext.tsx +0 -260
- package/src/context/NotificationContext.tsx +0 -280
- package/src/context/SchemaRendererContext.tsx +0 -48
- package/src/context/ThemeContext.tsx +0 -325
- package/src/context/__tests__/ActionContext.test.tsx +0 -174
- package/src/context/__tests__/DndContext.test.tsx +0 -150
- package/src/context/__tests__/NotificationContext.test.tsx +0 -176
- package/src/context/__tests__/useDataScope.test.tsx +0 -91
- package/src/context/index.ts +0 -13
- package/src/hooks/__tests__/useActionEngine.test.ts +0 -170
- package/src/hooks/__tests__/useActionRunner.test.ts +0 -153
- package/src/hooks/__tests__/useAnimation.test.ts +0 -82
- package/src/hooks/__tests__/useColumnSummary.test.ts +0 -93
- package/src/hooks/__tests__/useCrudShortcuts.test.ts +0 -85
- package/src/hooks/__tests__/useDataRefresh.test.ts +0 -115
- package/src/hooks/__tests__/useDebugMode.test.ts +0 -83
- package/src/hooks/__tests__/useDensityMode.test.ts +0 -81
- package/src/hooks/__tests__/useDiscovery.test.tsx +0 -247
- package/src/hooks/__tests__/useDynamicApp.test.ts +0 -161
- package/src/hooks/__tests__/useETagCache.test.ts +0 -153
- package/src/hooks/__tests__/useExpression.test.ts +0 -86
- package/src/hooks/__tests__/useFocusTrap.test.ts +0 -294
- package/src/hooks/__tests__/useKeyboardShortcuts.test.ts +0 -144
- package/src/hooks/__tests__/usePageTransition.test.ts +0 -158
- package/src/hooks/__tests__/usePerformanceBudget.test.ts +0 -157
- package/src/hooks/__tests__/useReducedMotion.test.ts +0 -104
- package/src/hooks/__tests__/useSchemaPersistence.test.ts +0 -220
- package/src/hooks/__tests__/useViewSharing.test.ts +0 -121
- package/src/hooks/__tests__/useViewTransition.test.ts +0 -61
- package/src/hooks/index.ts +0 -36
- package/src/hooks/useActionEngine.ts +0 -84
- package/src/hooks/useActionRunner.ts +0 -119
- package/src/hooks/useAnimation.ts +0 -117
- package/src/hooks/useClientNotifications.ts +0 -251
- package/src/hooks/useColumnSummary.ts +0 -87
- package/src/hooks/useCrudShortcuts.ts +0 -126
- package/src/hooks/useDataRefresh.ts +0 -58
- package/src/hooks/useDebugMode.ts +0 -95
- package/src/hooks/useDensityMode.ts +0 -102
- package/src/hooks/useDiscovery.ts +0 -168
- package/src/hooks/useDynamicApp.ts +0 -142
- package/src/hooks/useETagCache.ts +0 -404
- package/src/hooks/useExpression.ts +0 -56
- package/src/hooks/useFocusManagement.ts +0 -248
- package/src/hooks/useFocusTrap.ts +0 -138
- package/src/hooks/useGlobalUndo.ts +0 -98
- package/src/hooks/useKeyboardShortcuts.ts +0 -119
- package/src/hooks/useNavigationOverlay.ts +0 -190
- package/src/hooks/useOffline.ts +0 -301
- package/src/hooks/usePageTransition.ts +0 -187
- package/src/hooks/usePageVariables.tsx +0 -157
- package/src/hooks/usePerformance.ts +0 -246
- package/src/hooks/usePerformanceBudget.ts +0 -313
- package/src/hooks/useReducedMotion.ts +0 -53
- package/src/hooks/useSchemaPersistence.ts +0 -222
- package/src/hooks/useViewData.ts +0 -201
- package/src/hooks/useViewSharing.ts +0 -140
- package/src/hooks/useViewTransition.ts +0 -144
- package/src/index.ts +0 -41
- package/src/spec-bridge/SpecBridge.ts +0 -64
- package/src/spec-bridge/__tests__/P1SpecBridge.test.ts +0 -855
- package/src/spec-bridge/__tests__/SpecBridge.test.ts +0 -550
- package/src/spec-bridge/bridges/dashboard.ts +0 -180
- package/src/spec-bridge/bridges/form-view.ts +0 -109
- package/src/spec-bridge/bridges/list-view.ts +0 -187
- package/src/spec-bridge/bridges/page.ts +0 -136
- package/src/spec-bridge/index.ts +0 -14
- package/src/spec-bridge/types.ts +0 -38
- package/src/utils/i18n.ts +0 -10
- package/tsconfig.json +0 -17
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -162,6 +162,26 @@ The `previewMode` object contains:
|
|
|
162
162
|
|
|
163
163
|
See [full documentation](https://objectui.org/api/react) for detailed API reference.
|
|
164
164
|
|
|
165
|
+
<!-- release-metadata:v3.3.0 -->
|
|
166
|
+
|
|
167
|
+
## Compatibility
|
|
168
|
+
|
|
169
|
+
- **React:** 18.x or 19.x
|
|
170
|
+
- **Node.js:** ≥ 18
|
|
171
|
+
- **TypeScript:** ≥ 5.0 (strict mode)
|
|
172
|
+
- **`@objectstack/spec`:** ^3.3.0
|
|
173
|
+
- **`@objectstack/client`:** ^3.3.0
|
|
174
|
+
- **Tailwind CSS:** ≥ 3.4 (for packages with UI)
|
|
175
|
+
|
|
176
|
+
## Links
|
|
177
|
+
|
|
178
|
+
- 📚 [Documentation](https://www.objectui.org/docs/packages/react)
|
|
179
|
+
- 📦 [npm package](https://www.npmjs.com/package/@object-ui/react)
|
|
180
|
+
- 📝 [Changelog](./CHANGELOG.md)
|
|
181
|
+
- 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
|
|
182
|
+
- 🤝 [Contributing Guide](https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md)
|
|
183
|
+
- 🗺️ [Roadmap](https://github.com/objectstack-ai/objectui/blob/main/ROADMAP.md)
|
|
184
|
+
|
|
165
185
|
## License
|
|
166
186
|
|
|
167
|
-
MIT
|
|
187
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
2
|
+
export declare const AdapterCtx: import("react").Context<ObjectStackAdapter<unknown> | null>;
|
|
3
|
+
export declare function useAdapter(): ObjectStackAdapter | null;
|
|
4
|
+
export interface MetadataState {
|
|
5
|
+
apps: any[];
|
|
6
|
+
objects: any[];
|
|
7
|
+
dashboards: any[];
|
|
8
|
+
reports: any[];
|
|
9
|
+
pages: any[];
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
}
|
|
13
|
+
export interface MetadataContextValue extends MetadataState {
|
|
14
|
+
refresh: (type?: string) => Promise<void>;
|
|
15
|
+
invalidate: (type: string, name?: string) => void;
|
|
16
|
+
ensureType: (type: string) => Promise<any[]>;
|
|
17
|
+
getItem: (type: string, name: string) => Promise<any | null>;
|
|
18
|
+
getItemsByType: (type: string) => any[];
|
|
19
|
+
}
|
|
20
|
+
export declare const MetadataCtx: import("react").Context<MetadataContextValue | null>;
|
|
21
|
+
export declare function useMetadata(): MetadataContextValue;
|
|
22
|
+
export declare function useMetadataItem(type: string, name: string | undefined | null): {
|
|
23
|
+
item: any | null;
|
|
24
|
+
loading: boolean;
|
|
25
|
+
error: Error | null;
|
|
26
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// AdapterContext
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export const AdapterCtx = createContext(null);
|
|
6
|
+
export function useAdapter() {
|
|
7
|
+
return useContext(AdapterCtx);
|
|
8
|
+
}
|
|
9
|
+
export const MetadataCtx = createContext(null);
|
|
10
|
+
export function useMetadata() {
|
|
11
|
+
const ctx = useContext(MetadataCtx);
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
// Graceful fallback: when a consumer is rendered outside a MetadataProvider
|
|
14
|
+
// (common in unit tests that only need to assert on rendering), return an
|
|
15
|
+
// empty no-op implementation rather than crash. Production code paths
|
|
16
|
+
// should always wrap in <MetadataProvider>.
|
|
17
|
+
return {
|
|
18
|
+
apps: [],
|
|
19
|
+
objects: [],
|
|
20
|
+
dashboards: [],
|
|
21
|
+
reports: [],
|
|
22
|
+
pages: [],
|
|
23
|
+
loading: false,
|
|
24
|
+
error: null,
|
|
25
|
+
refresh: async () => { },
|
|
26
|
+
invalidate: () => { },
|
|
27
|
+
ensureType: async () => [],
|
|
28
|
+
getItem: async () => null,
|
|
29
|
+
getItemsByType: () => [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return ctx;
|
|
33
|
+
}
|
|
34
|
+
export function useMetadataItem(type, name) {
|
|
35
|
+
const { getItem } = useMetadata();
|
|
36
|
+
const [state, setState] = useState({
|
|
37
|
+
item: null,
|
|
38
|
+
loading: !!name,
|
|
39
|
+
error: null,
|
|
40
|
+
});
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!name) {
|
|
43
|
+
setState({ item: null, loading: false, error: null });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let cancelled = false;
|
|
47
|
+
setState(s => ({ ...s, loading: true, error: null }));
|
|
48
|
+
getItem(type, name)
|
|
49
|
+
.then(item => {
|
|
50
|
+
if (!cancelled)
|
|
51
|
+
setState({ item, loading: false, error: null });
|
|
52
|
+
})
|
|
53
|
+
.catch(err => {
|
|
54
|
+
if (!cancelled) {
|
|
55
|
+
setState({
|
|
56
|
+
item: null,
|
|
57
|
+
loading: false,
|
|
58
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
cancelled = true;
|
|
64
|
+
};
|
|
65
|
+
}, [type, name, getItem]);
|
|
66
|
+
return state;
|
|
67
|
+
}
|
package/dist/context/index.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
1
|
export * from './SchemaRendererContext';
|
|
9
2
|
export * from './ActionContext';
|
|
10
3
|
export * from './ThemeContext';
|
|
11
4
|
export * from './NotificationContext';
|
|
12
5
|
export * from './DndContext';
|
|
6
|
+
export * from './AppShellContext';
|
package/dist/context/index.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
1
|
export * from './SchemaRendererContext';
|
|
9
2
|
export * from './ActionContext';
|
|
10
3
|
export * from './ThemeContext';
|
|
11
4
|
export * from './NotificationContext';
|
|
12
5
|
export * from './DndContext';
|
|
6
|
+
export * from './AppShellContext';
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/react",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "React bindings and SchemaRenderer component for Object UI",
|
|
7
|
-
"homepage": "https://www.objectui.org",
|
|
7
|
+
"homepage": "https://www.objectui.org/docs/packages/react",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/objectstack-ai/objectui.git",
|
|
10
|
+
"url": "git+https://github.com/objectstack-ai/objectui.git",
|
|
11
11
|
"directory": "packages/react"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@objectstack/spec": "^4.0.
|
|
28
|
-
"react-hook-form": "^7.
|
|
29
|
-
"@object-ui/core": "3.3.
|
|
30
|
-
"@object-ui/
|
|
31
|
-
"@object-ui/
|
|
27
|
+
"@objectstack/spec": "^4.0.4",
|
|
28
|
+
"react-hook-form": "^7.75.0",
|
|
29
|
+
"@object-ui/core": "3.3.1",
|
|
30
|
+
"@object-ui/data-objectstack": "3.3.1",
|
|
31
|
+
"@object-ui/i18n": "3.3.1",
|
|
32
|
+
"@object-ui/types": "3.3.1"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -37,8 +38,30 @@
|
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/react": "19.2.14",
|
|
39
40
|
"@types/react-dom": "19.2.3",
|
|
40
|
-
"typescript": "^6.0.
|
|
41
|
+
"typescript": "^6.0.3"
|
|
41
42
|
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"objectui",
|
|
45
|
+
"sdui",
|
|
46
|
+
"schema-driven-ui",
|
|
47
|
+
"react",
|
|
48
|
+
"tailwind",
|
|
49
|
+
"shadcn",
|
|
50
|
+
"objectstack",
|
|
51
|
+
"renderer",
|
|
52
|
+
"schema-renderer",
|
|
53
|
+
"hooks"
|
|
54
|
+
],
|
|
55
|
+
"author": "ObjectStack Team <team@objectstack.ai>",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"files": [
|
|
60
|
+
"dist",
|
|
61
|
+
"README.md",
|
|
62
|
+
"CHANGELOG.md",
|
|
63
|
+
"LICENSE"
|
|
64
|
+
],
|
|
42
65
|
"scripts": {
|
|
43
66
|
"build": "tsc",
|
|
44
67
|
"test": "vitest run",
|
package/.turbo/turbo-build.log
DELETED
package/src/LazyPluginLoader.tsx
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React, { lazy, Suspense, Component } from 'react';
|
|
10
|
-
|
|
11
|
-
export interface LazyPluginOptions {
|
|
12
|
-
/**
|
|
13
|
-
* Fallback component to show while loading
|
|
14
|
-
*/
|
|
15
|
-
fallback?: React.ReactNode;
|
|
16
|
-
/**
|
|
17
|
-
* Number of retry attempts on load failure (default: 2)
|
|
18
|
-
*/
|
|
19
|
-
retries?: number;
|
|
20
|
-
/**
|
|
21
|
-
* Delay in ms between retries (default: 1000)
|
|
22
|
-
*/
|
|
23
|
-
retryDelay?: number;
|
|
24
|
-
/**
|
|
25
|
-
* Custom error fallback component
|
|
26
|
-
*/
|
|
27
|
-
errorFallback?: React.ComponentType<{ error: Error; retry: () => void }>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Error boundary for lazy-loaded plugins
|
|
32
|
-
*/
|
|
33
|
-
interface ErrorBoundaryState {
|
|
34
|
-
hasError: boolean;
|
|
35
|
-
error: Error | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
class PluginErrorBoundary extends Component<
|
|
39
|
-
{ fallback?: React.ComponentType<{ error: Error; retry: () => void }>; children: React.ReactNode },
|
|
40
|
-
ErrorBoundaryState
|
|
41
|
-
> {
|
|
42
|
-
state: ErrorBoundaryState = { hasError: false, error: null };
|
|
43
|
-
|
|
44
|
-
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
45
|
-
return { hasError: true, error };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
handleRetry = () => {
|
|
49
|
-
this.setState({ hasError: false, error: null });
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
render() {
|
|
53
|
-
if (this.state.hasError && this.state.error) {
|
|
54
|
-
const FallbackComponent = this.props.fallback;
|
|
55
|
-
if (FallbackComponent) {
|
|
56
|
-
return <FallbackComponent error={this.state.error} retry={this.handleRetry} />;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
return this.props.children;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a lazy-loaded import function with retry support
|
|
66
|
-
*/
|
|
67
|
-
function createRetryImport<P extends object>(
|
|
68
|
-
importFn: () => Promise<{ default: React.ComponentType<P> }>,
|
|
69
|
-
retries: number,
|
|
70
|
-
retryDelay: number,
|
|
71
|
-
): () => Promise<{ default: React.ComponentType<P> }> {
|
|
72
|
-
return () => {
|
|
73
|
-
let attempt = 0;
|
|
74
|
-
const tryImport = (): Promise<{ default: React.ComponentType<P> }> =>
|
|
75
|
-
importFn().catch((err) => {
|
|
76
|
-
attempt++;
|
|
77
|
-
if (attempt <= retries) {
|
|
78
|
-
return new Promise((resolve) =>
|
|
79
|
-
setTimeout(() => resolve(tryImport()), retryDelay),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
throw err;
|
|
83
|
-
});
|
|
84
|
-
return tryImport();
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
90
|
-
*
|
|
91
|
-
* @param importFn - Dynamic import function that returns a module with default export
|
|
92
|
-
* @param options - Configuration options for the lazy plugin
|
|
93
|
-
* @returns A component that lazy loads the plugin
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* ```tsx
|
|
97
|
-
* // Basic usage
|
|
98
|
-
* const ObjectGrid = createLazyPlugin(
|
|
99
|
-
* () => import('@object-ui/plugin-grid')
|
|
100
|
-
* );
|
|
101
|
-
*
|
|
102
|
-
* // With custom fallback
|
|
103
|
-
* const ObjectGrid = createLazyPlugin(
|
|
104
|
-
* () => import('@object-ui/plugin-grid'),
|
|
105
|
-
* { fallback: <div>Loading grid...</div> }
|
|
106
|
-
* );
|
|
107
|
-
*
|
|
108
|
-
* // With retry and error handling
|
|
109
|
-
* const ObjectGrid = createLazyPlugin(
|
|
110
|
-
* () => import('@object-ui/plugin-grid'),
|
|
111
|
-
* {
|
|
112
|
-
* retries: 3,
|
|
113
|
-
* errorFallback: ({ error, retry }) => (
|
|
114
|
-
* <div>
|
|
115
|
-
* <p>Failed to load: {error.message}</p>
|
|
116
|
-
* <button onClick={retry}>Retry</button>
|
|
117
|
-
* </div>
|
|
118
|
-
* ),
|
|
119
|
-
* }
|
|
120
|
-
* );
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
|
-
export function createLazyPlugin<P extends object = any>(
|
|
124
|
-
importFn: () => Promise<{ default: React.ComponentType<P> }>,
|
|
125
|
-
options?: LazyPluginOptions
|
|
126
|
-
): React.ComponentType<P> {
|
|
127
|
-
const { retries = 2, retryDelay = 1000, errorFallback } = options || {};
|
|
128
|
-
|
|
129
|
-
const retryImport = retries > 0
|
|
130
|
-
? createRetryImport(importFn, retries, retryDelay)
|
|
131
|
-
: importFn;
|
|
132
|
-
|
|
133
|
-
const LazyComponent = lazy(retryImport);
|
|
134
|
-
|
|
135
|
-
const PluginWrapper: React.FC<P> = (props) => {
|
|
136
|
-
const content = (
|
|
137
|
-
<Suspense fallback={options?.fallback || null}>
|
|
138
|
-
<LazyComponent {...props} />
|
|
139
|
-
</Suspense>
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
if (errorFallback) {
|
|
143
|
-
return (
|
|
144
|
-
<PluginErrorBoundary fallback={errorFallback}>
|
|
145
|
-
{content}
|
|
146
|
-
</PluginErrorBoundary>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return content;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
return PluginWrapper;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Preload a plugin module without rendering it.
|
|
158
|
-
* Useful for preloading plugins that will be needed soon.
|
|
159
|
-
*
|
|
160
|
-
* @param importFn - Dynamic import function
|
|
161
|
-
* @returns Promise that resolves when the module is loaded
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* ```tsx
|
|
165
|
-
* // Preload on hover
|
|
166
|
-
* const loadGrid = () => import('@object-ui/plugin-grid');
|
|
167
|
-
* <button onMouseEnter={() => preloadPlugin(loadGrid)}>Show Grid</button>
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
export function preloadPlugin<T = unknown>(
|
|
171
|
-
importFn: () => Promise<T>,
|
|
172
|
-
): Promise<T> {
|
|
173
|
-
return importFn();
|
|
174
|
-
}
|
package/src/SchemaRenderer.tsx
DELETED
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React, { forwardRef, useContext, useMemo, Component } from 'react';
|
|
10
|
-
import {
|
|
11
|
-
SchemaNode,
|
|
12
|
-
ComponentRegistry,
|
|
13
|
-
ExpressionEvaluator,
|
|
14
|
-
isObjectUIError,
|
|
15
|
-
type ObjectUIError,
|
|
16
|
-
ERROR_CODES,
|
|
17
|
-
debugLog,
|
|
18
|
-
debugTime,
|
|
19
|
-
debugTimeEnd,
|
|
20
|
-
DebugCollector,
|
|
21
|
-
} from '@object-ui/core';
|
|
22
|
-
import { SchemaRendererContext } from './context/SchemaRendererContext';
|
|
23
|
-
import { resolveI18nLabel } from './utils/i18n';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Extract AriaPropsSchema properties from a schema node and convert
|
|
27
|
-
* them to standard HTML ARIA attributes.
|
|
28
|
-
*
|
|
29
|
-
* @objectstack/spec AriaPropsSchema defines:
|
|
30
|
-
* ariaLabel: string | I18nLabel (→ aria-label)
|
|
31
|
-
* ariaDescribedBy: string (→ aria-describedby)
|
|
32
|
-
* role: string (→ role)
|
|
33
|
-
*/
|
|
34
|
-
function resolveAriaProps(schema: Record<string, any>): Record<string, string | undefined> {
|
|
35
|
-
const aria: Record<string, string | undefined> = {};
|
|
36
|
-
if (schema.ariaLabel) {
|
|
37
|
-
aria['aria-label'] = resolveI18nLabel(schema.ariaLabel);
|
|
38
|
-
}
|
|
39
|
-
if (schema.ariaDescribedBy) {
|
|
40
|
-
aria['aria-describedby'] = schema.ariaDescribedBy;
|
|
41
|
-
}
|
|
42
|
-
if (schema.role) {
|
|
43
|
-
aria['role'] = schema.role;
|
|
44
|
-
}
|
|
45
|
-
return aria;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Per-component Error Boundary for SchemaRenderer.
|
|
50
|
-
* Catches render errors in individual components, preventing one broken
|
|
51
|
-
* component from crashing the entire page.
|
|
52
|
-
*/
|
|
53
|
-
interface SchemaErrorBoundaryState {
|
|
54
|
-
hasError: boolean;
|
|
55
|
-
error: Error | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export class SchemaErrorBoundary extends Component<
|
|
59
|
-
{ componentType?: string; children: React.ReactNode },
|
|
60
|
-
SchemaErrorBoundaryState
|
|
61
|
-
> {
|
|
62
|
-
state: SchemaErrorBoundaryState = { hasError: false, error: null };
|
|
63
|
-
|
|
64
|
-
static getDerivedStateFromError(error: Error): SchemaErrorBoundaryState {
|
|
65
|
-
return { hasError: true, error };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
handleRetry = () => {
|
|
69
|
-
this.setState({ hasError: false, error: null });
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
render() {
|
|
73
|
-
if (this.state.hasError && this.state.error) {
|
|
74
|
-
const error = this.state.error;
|
|
75
|
-
const isDev = (globalThis as any).process?.env?.NODE_ENV !== 'production';
|
|
76
|
-
const objuiError = isObjectUIError(error) ? error as ObjectUIError : null;
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div className="p-4 border border-orange-400 rounded bg-orange-50 text-orange-700 my-2" role="alert">
|
|
80
|
-
<p className="font-medium">
|
|
81
|
-
Component{this.props.componentType ? ` "${this.props.componentType}"` : ''} failed to render
|
|
82
|
-
</p>
|
|
83
|
-
<p className="text-sm mt-1">{error.message}</p>
|
|
84
|
-
{isDev && objuiError?.code && (
|
|
85
|
-
<p className="text-xs mt-1 text-orange-500">
|
|
86
|
-
Error code: {objuiError.code}
|
|
87
|
-
{objuiError.details?.suggestion ? (
|
|
88
|
-
<span className="block mt-0.5">💡 {String(objuiError.details.suggestion)}</span>
|
|
89
|
-
) : null}
|
|
90
|
-
</p>
|
|
91
|
-
)}
|
|
92
|
-
<button
|
|
93
|
-
onClick={this.handleRetry}
|
|
94
|
-
className="mt-2 text-sm underline hover:no-underline"
|
|
95
|
-
>
|
|
96
|
-
Retry
|
|
97
|
-
</button>
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return this.props.children;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export const SchemaRenderer = forwardRef<any, { schema: SchemaNode } & Record<string, any>>(({ schema, ...props }, _ref) => {
|
|
106
|
-
const context = useContext(SchemaRendererContext);
|
|
107
|
-
const dataSource = context?.dataSource || {};
|
|
108
|
-
|
|
109
|
-
// Evaluate schema expressions against the data source
|
|
110
|
-
const evaluatedSchema = useMemo(() => {
|
|
111
|
-
if (!schema || typeof schema === 'string') return schema;
|
|
112
|
-
|
|
113
|
-
const evaluator = new ExpressionEvaluator({ data: dataSource });
|
|
114
|
-
// Shallow copy
|
|
115
|
-
const newSchema = { ...schema };
|
|
116
|
-
|
|
117
|
-
// COMPAT: Hoist 'properties' up to schema level
|
|
118
|
-
// This allows support for strict configs that wrap all props in 'properties'
|
|
119
|
-
if (newSchema.properties) {
|
|
120
|
-
Object.assign(newSchema, newSchema.properties);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Evaluate 'content' (common in Text, Button)
|
|
124
|
-
if (typeof newSchema.content === 'string') {
|
|
125
|
-
newSchema.content = evaluator.evaluate(newSchema.content);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Evaluate 'props'
|
|
129
|
-
if (newSchema.props) {
|
|
130
|
-
const newProps = { ...newSchema.props };
|
|
131
|
-
for (const [key, val] of Object.entries(newProps)) {
|
|
132
|
-
newProps[key] = evaluator.evaluate(val as any);
|
|
133
|
-
}
|
|
134
|
-
newSchema.props = newProps;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Evaluate visibility: visible / visibleOn / hidden / hiddenOn
|
|
138
|
-
const shouldHide = (() => {
|
|
139
|
-
if (newSchema.visible !== undefined) {
|
|
140
|
-
return !evaluator.evaluateCondition(newSchema.visible);
|
|
141
|
-
}
|
|
142
|
-
if (newSchema.visibleOn !== undefined) {
|
|
143
|
-
return !evaluator.evaluateCondition(newSchema.visibleOn);
|
|
144
|
-
}
|
|
145
|
-
if (newSchema.hidden !== undefined) {
|
|
146
|
-
return evaluator.evaluateCondition(newSchema.hidden);
|
|
147
|
-
}
|
|
148
|
-
if (newSchema.hiddenOn !== undefined) {
|
|
149
|
-
return evaluator.evaluateCondition(newSchema.hiddenOn);
|
|
150
|
-
}
|
|
151
|
-
return false;
|
|
152
|
-
})();
|
|
153
|
-
|
|
154
|
-
if (shouldHide) {
|
|
155
|
-
newSchema._hidden = true;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Evaluate disabled: disabled / disabledOn
|
|
159
|
-
const isDisabled = (() => {
|
|
160
|
-
if (newSchema.disabled !== undefined) {
|
|
161
|
-
return evaluator.evaluateCondition(newSchema.disabled);
|
|
162
|
-
}
|
|
163
|
-
if (newSchema.disabledOn !== undefined) {
|
|
164
|
-
return evaluator.evaluateCondition(newSchema.disabledOn);
|
|
165
|
-
}
|
|
166
|
-
return false;
|
|
167
|
-
})();
|
|
168
|
-
|
|
169
|
-
if (isDisabled) {
|
|
170
|
-
newSchema._disabled = true;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return newSchema;
|
|
174
|
-
}, [schema, dataSource]);
|
|
175
|
-
|
|
176
|
-
if (!evaluatedSchema) return null;
|
|
177
|
-
// Handle visibility: if evaluated schema is hidden, render nothing
|
|
178
|
-
if (evaluatedSchema?._hidden) return null;
|
|
179
|
-
// If schema is just a string, render it as text
|
|
180
|
-
if (typeof evaluatedSchema === 'string') return <>{evaluatedSchema}</>;
|
|
181
|
-
|
|
182
|
-
debugLog('schema', 'Rendering schema node', { type: evaluatedSchema.type, id: evaluatedSchema.id });
|
|
183
|
-
|
|
184
|
-
const Component = ComponentRegistry.get(evaluatedSchema.type);
|
|
185
|
-
|
|
186
|
-
if (!Component) {
|
|
187
|
-
debugLog('schema', 'Component not found in registry', { type: evaluatedSchema.type });
|
|
188
|
-
const errorInfo = ERROR_CODES['OBJUI-001'];
|
|
189
|
-
return (
|
|
190
|
-
<div className="p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2" role="alert">
|
|
191
|
-
<p className="font-medium">Unknown component type: <strong>{evaluatedSchema.type}</strong></p>
|
|
192
|
-
{(globalThis as any).process?.env?.NODE_ENV !== 'production' && (
|
|
193
|
-
<p className="text-xs mt-1">💡 {errorInfo.suggestion} (OBJUI-001)</p>
|
|
194
|
-
)}
|
|
195
|
-
<pre className="text-xs mt-2 overflow-auto">{JSON.stringify(evaluatedSchema, null, 2)}</pre>
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Note: We don't forward the ref to the Component because components in the registry
|
|
201
|
-
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
202
|
-
|
|
203
|
-
// Extract schema metadata properties that should NOT be passed as React props
|
|
204
|
-
const {
|
|
205
|
-
type: _type,
|
|
206
|
-
children: _children,
|
|
207
|
-
body: _body,
|
|
208
|
-
schema: _schema,
|
|
209
|
-
visible: _visible,
|
|
210
|
-
visibleOn: _visibleOn,
|
|
211
|
-
hidden: _hidden,
|
|
212
|
-
hiddenOn: _hiddenOn,
|
|
213
|
-
disabled: _disabled,
|
|
214
|
-
disabledOn: _disabledOn,
|
|
215
|
-
_hidden: __hidden, // stripped: internal visibility flag
|
|
216
|
-
_disabled: __disabled, // stripped: internal disabled flag
|
|
217
|
-
...componentProps
|
|
218
|
-
} = evaluatedSchema;
|
|
219
|
-
|
|
220
|
-
// Extract AriaPropsSchema properties for accessibility
|
|
221
|
-
const ariaProps = resolveAriaProps(evaluatedSchema);
|
|
222
|
-
|
|
223
|
-
// Debug-mode enhancements: extra data attributes + perf tracking
|
|
224
|
-
const isDebug = context?.debug || context?.debugFlags?.enabled;
|
|
225
|
-
const debugAttrs: Record<string, string> = {};
|
|
226
|
-
if (isDebug) {
|
|
227
|
-
debugAttrs['data-debug-type'] = evaluatedSchema.type;
|
|
228
|
-
if (evaluatedSchema.id) {
|
|
229
|
-
debugAttrs['data-debug-id'] = evaluatedSchema.id;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
debugTime(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
234
|
-
const renderStart = isDebug ? performance.now() : 0;
|
|
235
|
-
const rendered = (
|
|
236
|
-
<SchemaErrorBoundary componentType={evaluatedSchema.type}>
|
|
237
|
-
{React.createElement(Component, {
|
|
238
|
-
schema: evaluatedSchema,
|
|
239
|
-
...componentProps, // Spread non-metadata schema properties as props
|
|
240
|
-
...(evaluatedSchema.props || {}), // Override with explicit props if provided
|
|
241
|
-
...ariaProps, // Inject ARIA attributes from AriaPropsSchema
|
|
242
|
-
...debugAttrs, // Debug-mode data attributes
|
|
243
|
-
disabled: __disabled || undefined,
|
|
244
|
-
className: evaluatedSchema.className,
|
|
245
|
-
'data-obj-id': evaluatedSchema.id,
|
|
246
|
-
'data-obj-type': evaluatedSchema.type,
|
|
247
|
-
...props
|
|
248
|
-
})}
|
|
249
|
-
</SchemaErrorBoundary>
|
|
250
|
-
);
|
|
251
|
-
debugTimeEnd(`render:${evaluatedSchema.type}:${evaluatedSchema.id ?? 'anon'}`);
|
|
252
|
-
|
|
253
|
-
// Report render perf to DebugCollector when debug mode is active
|
|
254
|
-
if (isDebug && renderStart) {
|
|
255
|
-
const durationMs = performance.now() - renderStart;
|
|
256
|
-
DebugCollector.getInstance().addPerf({
|
|
257
|
-
type: evaluatedSchema.type,
|
|
258
|
-
id: evaluatedSchema.id,
|
|
259
|
-
durationMs,
|
|
260
|
-
timestamp: Date.now(),
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return rendered;
|
|
265
|
-
});
|
|
266
|
-
SchemaRenderer.displayName = 'SchemaRenderer';
|