@object-ui/react 3.3.0 → 3.3.2
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 +18 -0
- package/README.md +21 -1
- package/dist/SchemaRenderer.js +19 -2
- 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
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @object-ui/react
|
|
2
2
|
|
|
3
|
+
## 3.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @object-ui/types@3.3.2
|
|
8
|
+
- @object-ui/core@3.3.2
|
|
9
|
+
- @object-ui/i18n@3.3.2
|
|
10
|
+
- @object-ui/data-objectstack@3.3.2
|
|
11
|
+
|
|
12
|
+
## 3.3.1
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- @object-ui/types@3.3.1
|
|
17
|
+
- @object-ui/core@3.3.1
|
|
18
|
+
- @object-ui/i18n@3.3.1
|
|
19
|
+
- @object-ui/data-objectstack@3.3.1
|
|
20
|
+
|
|
3
21
|
## 3.3.0
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
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).
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -6,7 +6,7 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
* This source code is licensed under the MIT license found in the
|
|
7
7
|
* LICENSE file in the root directory of this source tree.
|
|
8
8
|
*/
|
|
9
|
-
import React, { forwardRef, useContext, useMemo, Component } from 'react';
|
|
9
|
+
import React, { forwardRef, useContext, useMemo, useEffect, useReducer, useState, Component } from 'react';
|
|
10
10
|
import { ComponentRegistry, ExpressionEvaluator, isObjectUIError, ERROR_CODES, debugLog, debugTime, debugTimeEnd, DebugCollector, } from '@object-ui/core';
|
|
11
11
|
import { SchemaRendererContext } from './context/SchemaRendererContext';
|
|
12
12
|
import { resolveI18nLabel } from './utils/i18n';
|
|
@@ -66,6 +66,11 @@ export class SchemaErrorBoundary extends Component {
|
|
|
66
66
|
export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
67
67
|
const context = useContext(SchemaRendererContext);
|
|
68
68
|
const dataSource = context?.dataSource || {};
|
|
69
|
+
// Re-render trigger when the global ComponentRegistry mutates (e.g. a
|
|
70
|
+
// lazy-loaded plugin finishes registering its components).
|
|
71
|
+
const [, forceUpdate] = useReducer((n) => n + 1, 0);
|
|
72
|
+
useEffect(() => ComponentRegistry.subscribe(forceUpdate), []);
|
|
73
|
+
const [lazyError, setLazyError] = useState(null);
|
|
69
74
|
// Evaluate schema expressions against the data source
|
|
70
75
|
const evaluatedSchema = useMemo(() => {
|
|
71
76
|
if (!schema || typeof schema === 'string')
|
|
@@ -135,9 +140,21 @@ export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
|
135
140
|
debugLog('schema', 'Rendering schema node', { type: evaluatedSchema.type, id: evaluatedSchema.id });
|
|
136
141
|
const Component = ComponentRegistry.get(evaluatedSchema.type);
|
|
137
142
|
if (!Component) {
|
|
143
|
+
// If a lazy loader is registered for this type, kick it off — the
|
|
144
|
+
// registry will notify us via subscribe() once the plugin module's
|
|
145
|
+
// top-level register() side-effects have run.
|
|
146
|
+
if (!lazyError && ComponentRegistry.hasLazy(evaluatedSchema.type)) {
|
|
147
|
+
const pending = ComponentRegistry.loadLazy(evaluatedSchema.type);
|
|
148
|
+
if (pending) {
|
|
149
|
+
pending.catch((err) => {
|
|
150
|
+
setLazyError(err instanceof Error ? err : new Error(String(err)));
|
|
151
|
+
});
|
|
152
|
+
return (_jsxs("div", { className: "p-2 text-sm text-muted-foreground animate-pulse", role: "status", "aria-live": "polite", "data-lazy-loading": evaluatedSchema.type, children: ["Loading ", _jsx("code", { children: evaluatedSchema.type }), "\u2026"] }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
138
155
|
debugLog('schema', 'Component not found in registry', { type: evaluatedSchema.type });
|
|
139
156
|
const errorInfo = ERROR_CODES['OBJUI-001'];
|
|
140
|
-
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Unknown component type: ", _jsx("strong", { children: evaluatedSchema.type })] }), globalThis.process?.env?.NODE_ENV !== 'production' && (_jsxs("p", { className: "text-xs mt-1", children: ["\uD83D\uDCA1 ", errorInfo.suggestion, " (OBJUI-001)"] })), _jsx("pre", { className: "text-xs mt-2 overflow-auto", children: JSON.stringify(evaluatedSchema, null, 2) })] }));
|
|
157
|
+
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", role: "alert", children: [_jsxs("p", { className: "font-medium", children: ["Unknown component type: ", _jsx("strong", { children: evaluatedSchema.type })] }), lazyError && (_jsxs("p", { className: "text-xs mt-1", children: ["Failed to load plugin: ", lazyError.message] })), globalThis.process?.env?.NODE_ENV !== 'production' && (_jsxs("p", { className: "text-xs mt-1", children: ["\uD83D\uDCA1 ", errorInfo.suggestion, " (OBJUI-001)"] })), _jsx("pre", { className: "text-xs mt-2 overflow-auto", children: JSON.stringify(evaluatedSchema, null, 2) })] }));
|
|
141
158
|
}
|
|
142
159
|
// Note: We don't forward the ref to the Component because components in the registry
|
|
143
160
|
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
@@ -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.2",
|
|
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.2",
|
|
30
|
+
"@object-ui/data-objectstack": "3.3.2",
|
|
31
|
+
"@object-ui/i18n": "3.3.2",
|
|
32
|
+
"@object-ui/types": "3.3.2"
|
|
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
|
-
}
|