@object-ui/react 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/dist/LazyPluginLoader.d.ts +38 -0
- package/dist/LazyPluginLoader.js +35 -0
- package/dist/SchemaRenderer.js +44 -12
- package/dist/__tests__/LazyPluginLoader.test.d.ts +8 -0
- package/dist/__tests__/LazyPluginLoader.test.js +49 -0
- package/dist/context/SchemaRendererContext.d.ts +2 -1
- package/dist/context/SchemaRendererContext.js +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +14 -6
- package/src/LazyPluginLoader.tsx +52 -0
- package/src/SchemaRenderer.tsx +64 -12
- package/src/__tests__/LazyPluginLoader.test.tsx +80 -0
- package/src/context/SchemaRendererContext.tsx +5 -2
- package/src/index.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import React from 'react';
|
|
9
|
+
export interface LazyPluginOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Fallback component to show while loading
|
|
12
|
+
*/
|
|
13
|
+
fallback?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
17
|
+
*
|
|
18
|
+
* @param importFn - Dynamic import function that returns a module with default export
|
|
19
|
+
* @param options - Configuration options for the lazy plugin
|
|
20
|
+
* @returns A component that lazy loads the plugin
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* // Basic usage
|
|
25
|
+
* const ObjectGrid = createLazyPlugin(
|
|
26
|
+
* () => import('@object-ui/plugin-grid')
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* // With custom fallback
|
|
30
|
+
* const ObjectGrid = createLazyPlugin(
|
|
31
|
+
* () => import('@object-ui/plugin-grid'),
|
|
32
|
+
* { fallback: <div>Loading grid...</div> }
|
|
33
|
+
* );
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function createLazyPlugin<P extends object = any>(importFn: () => Promise<{
|
|
37
|
+
default: React.ComponentType<P>;
|
|
38
|
+
}>, options?: LazyPluginOptions): React.ComponentType<P>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
import { lazy, Suspense } from 'react';
|
|
10
|
+
/**
|
|
11
|
+
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
12
|
+
*
|
|
13
|
+
* @param importFn - Dynamic import function that returns a module with default export
|
|
14
|
+
* @param options - Configuration options for the lazy plugin
|
|
15
|
+
* @returns A component that lazy loads the plugin
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // Basic usage
|
|
20
|
+
* const ObjectGrid = createLazyPlugin(
|
|
21
|
+
* () => import('@object-ui/plugin-grid')
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* // With custom fallback
|
|
25
|
+
* const ObjectGrid = createLazyPlugin(
|
|
26
|
+
* () => import('@object-ui/plugin-grid'),
|
|
27
|
+
* { fallback: <div>Loading grid...</div> }
|
|
28
|
+
* );
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function createLazyPlugin(importFn, options) {
|
|
32
|
+
const LazyComponent = lazy(importFn);
|
|
33
|
+
const PluginWrapper = (props) => (_jsx(Suspense, { fallback: options?.fallback || null, children: _jsx(LazyComponent, { ...props }) }));
|
|
34
|
+
return PluginWrapper;
|
|
35
|
+
}
|
package/dist/SchemaRenderer.js
CHANGED
|
@@ -6,26 +6,58 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } 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 } from 'react';
|
|
10
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
9
|
+
import React, { forwardRef, useContext, useMemo } from 'react';
|
|
10
|
+
import { ComponentRegistry, ExpressionEvaluator } from '@object-ui/core';
|
|
11
|
+
import { SchemaRendererContext } from './context/SchemaRendererContext';
|
|
11
12
|
export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
|
|
12
|
-
|
|
13
|
+
const context = useContext(SchemaRendererContext);
|
|
14
|
+
const dataSource = context?.dataSource || {};
|
|
15
|
+
// Evaluate schema expressions against the data source
|
|
16
|
+
const evaluatedSchema = useMemo(() => {
|
|
17
|
+
if (!schema || typeof schema === 'string')
|
|
18
|
+
return schema;
|
|
19
|
+
const evaluator = new ExpressionEvaluator({ data: dataSource });
|
|
20
|
+
// Shallow copy
|
|
21
|
+
const newSchema = { ...schema };
|
|
22
|
+
// COMPAT: Hoist 'properties' up to schema level
|
|
23
|
+
// This allows support for strict configs that wrap all props in 'properties'
|
|
24
|
+
if (newSchema.properties) {
|
|
25
|
+
Object.assign(newSchema, newSchema.properties);
|
|
26
|
+
}
|
|
27
|
+
// Evaluate 'content' (common in Text, Button)
|
|
28
|
+
if (typeof newSchema.content === 'string') {
|
|
29
|
+
newSchema.content = evaluator.evaluate(newSchema.content);
|
|
30
|
+
}
|
|
31
|
+
// Evaluate 'props'
|
|
32
|
+
if (newSchema.props) {
|
|
33
|
+
const newProps = { ...newSchema.props };
|
|
34
|
+
for (const [key, val] of Object.entries(newProps)) {
|
|
35
|
+
newProps[key] = evaluator.evaluate(val);
|
|
36
|
+
}
|
|
37
|
+
newSchema.props = newProps;
|
|
38
|
+
}
|
|
39
|
+
return newSchema;
|
|
40
|
+
}, [schema, dataSource]);
|
|
41
|
+
if (!evaluatedSchema)
|
|
13
42
|
return null;
|
|
14
43
|
// If schema is just a string, render it as text
|
|
15
|
-
if (typeof
|
|
16
|
-
return _jsx(_Fragment, { children:
|
|
17
|
-
const Component = ComponentRegistry.get(
|
|
44
|
+
if (typeof evaluatedSchema === 'string')
|
|
45
|
+
return _jsx(_Fragment, { children: evaluatedSchema });
|
|
46
|
+
const Component = ComponentRegistry.get(evaluatedSchema.type);
|
|
18
47
|
if (!Component) {
|
|
19
|
-
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", children: ["Unknown component type: ", _jsx("strong", { children:
|
|
48
|
+
return (_jsxs("div", { className: "p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2", children: ["Unknown component type: ", _jsx("strong", { children: evaluatedSchema.type }), _jsx("pre", { className: "text-xs mt-2 overflow-auto", children: JSON.stringify(evaluatedSchema, null, 2) })] }));
|
|
20
49
|
}
|
|
21
50
|
// Note: We don't forward the ref to the Component because components in the registry
|
|
22
51
|
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
52
|
+
// Extract schema metadata properties that should NOT be passed as React props
|
|
53
|
+
const { type: _type, children: _children, body: _body, schema: _schema, visible: _visible, visibleOn: _visibleOn, hidden: _hidden, hiddenOn: _hiddenOn, disabled: _disabled, disabledOn: _disabledOn, ...componentProps } = evaluatedSchema;
|
|
23
54
|
return React.createElement(Component, {
|
|
24
|
-
schema,
|
|
25
|
-
...
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'data-obj-
|
|
55
|
+
schema: evaluatedSchema,
|
|
56
|
+
...componentProps, // Spread non-metadata schema properties as props
|
|
57
|
+
...(evaluatedSchema.props || {}), // Override with explicit props if provided
|
|
58
|
+
className: evaluatedSchema.className,
|
|
59
|
+
'data-obj-id': evaluatedSchema.id,
|
|
60
|
+
'data-obj-type': evaluatedSchema.type,
|
|
29
61
|
...props
|
|
30
62
|
});
|
|
31
63
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import { createLazyPlugin } from '../LazyPluginLoader';
|
|
12
|
+
describe('createLazyPlugin', () => {
|
|
13
|
+
it('should create a lazy-loaded component', async () => {
|
|
14
|
+
const TestComponent = () => _jsx("div", { children: "Test Component" });
|
|
15
|
+
const LazyComponent = createLazyPlugin(() => Promise.resolve({ default: TestComponent }));
|
|
16
|
+
render(_jsx(LazyComponent, {}));
|
|
17
|
+
await waitFor(() => {
|
|
18
|
+
expect(screen.getByText('Test Component')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('should show custom fallback while loading', async () => {
|
|
22
|
+
const TestComponent = () => _jsx("div", { children: "Test Component" });
|
|
23
|
+
const LazyComponent = createLazyPlugin(() => new Promise((resolve) => {
|
|
24
|
+
setTimeout(() => resolve({ default: TestComponent }), 100);
|
|
25
|
+
}), { fallback: _jsx("div", { children: "Loading..." }) });
|
|
26
|
+
render(_jsx(LazyComponent, {}));
|
|
27
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
expect(screen.getByText('Test Component')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('should pass props to the lazy component', async () => {
|
|
33
|
+
const TestComponent = ({ message }) => _jsx("div", { children: message });
|
|
34
|
+
const LazyComponent = createLazyPlugin(() => Promise.resolve({ default: TestComponent }));
|
|
35
|
+
render(_jsx(LazyComponent, { message: "Hello World" }));
|
|
36
|
+
await waitFor(() => {
|
|
37
|
+
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it('should handle loading errors gracefully', async () => {
|
|
41
|
+
// Mock console.error to avoid noise in test output
|
|
42
|
+
const originalError = console.error;
|
|
43
|
+
console.error = vi.fn();
|
|
44
|
+
const LazyComponent = createLazyPlugin(() => Promise.reject(new Error('Load error')));
|
|
45
|
+
expect(() => render(_jsx(LazyComponent, {}))).not.toThrow();
|
|
46
|
+
// Restore console.error
|
|
47
|
+
console.error = originalError;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -3,6 +3,8 @@ interface SchemaRendererContextType {
|
|
|
3
3
|
dataSource: any;
|
|
4
4
|
debug?: boolean;
|
|
5
5
|
}
|
|
6
|
+
declare const SchemaRendererContext: React.Context<SchemaRendererContextType | null>;
|
|
7
|
+
export { SchemaRendererContext };
|
|
6
8
|
export declare const SchemaRendererProvider: ({ children, dataSource, debug }: {
|
|
7
9
|
children: React.ReactNode;
|
|
8
10
|
dataSource: any;
|
|
@@ -10,4 +12,3 @@ export declare const SchemaRendererProvider: ({ children, dataSource, debug }: {
|
|
|
10
12
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
11
13
|
export declare const useSchemaContext: () => SchemaRendererContextType;
|
|
12
14
|
export declare const useDataScope: (path?: string) => any;
|
|
13
|
-
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useMemo } from 'react';
|
|
3
3
|
const SchemaRendererContext = createContext(null);
|
|
4
|
+
export { SchemaRendererContext };
|
|
4
5
|
export const SchemaRendererProvider = ({ children, dataSource, debug }) => {
|
|
5
6
|
const value = useMemo(() => ({ dataSource, debug }), [dataSource, debug]);
|
|
6
7
|
return (_jsx(SchemaRendererContext.Provider, { value: value, children: children }));
|
|
@@ -13,8 +14,9 @@ export const useSchemaContext = () => {
|
|
|
13
14
|
return context;
|
|
14
15
|
};
|
|
15
16
|
export const useDataScope = (path) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
17
|
+
const context = useContext(SchemaRendererContext);
|
|
18
|
+
const dataSource = context?.dataSource;
|
|
19
|
+
if (!dataSource || !path)
|
|
18
20
|
return dataSource;
|
|
19
21
|
// Simple path resolution for now. In real app might be more complex
|
|
20
22
|
return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"description": "React bindings and SchemaRenderer component for Object UI",
|
|
6
7
|
"homepage": "https://www.objectui.org",
|
|
@@ -12,19 +13,26 @@
|
|
|
12
13
|
"bugs": {
|
|
13
14
|
"url": "https://github.com/objectstack-ai/objectui/issues"
|
|
14
15
|
},
|
|
15
|
-
"main": "dist/index.js",
|
|
16
|
-
"
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
17
25
|
"dependencies": {
|
|
18
|
-
"@objectstack/spec": "^0.
|
|
26
|
+
"@objectstack/spec": "^0.9.1",
|
|
19
27
|
"react-hook-form": "^7.71.1",
|
|
20
|
-
"@object-ui/core": "0.
|
|
28
|
+
"@object-ui/core": "0.5.0"
|
|
21
29
|
},
|
|
22
30
|
"peerDependencies": {
|
|
23
31
|
"react": "^18.0.0 || ^19.0.0",
|
|
24
32
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
25
33
|
},
|
|
26
34
|
"devDependencies": {
|
|
27
|
-
"@types/react": "^19.2.
|
|
35
|
+
"@types/react": "^19.2.10",
|
|
28
36
|
"@types/react-dom": "^19.2.3",
|
|
29
37
|
"typescript": "^5.9.3"
|
|
30
38
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
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 } from 'react';
|
|
10
|
+
|
|
11
|
+
export interface LazyPluginOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Fallback component to show while loading
|
|
14
|
+
*/
|
|
15
|
+
fallback?: React.ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
20
|
+
*
|
|
21
|
+
* @param importFn - Dynamic import function that returns a module with default export
|
|
22
|
+
* @param options - Configuration options for the lazy plugin
|
|
23
|
+
* @returns A component that lazy loads the plugin
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* // Basic usage
|
|
28
|
+
* const ObjectGrid = createLazyPlugin(
|
|
29
|
+
* () => import('@object-ui/plugin-grid')
|
|
30
|
+
* );
|
|
31
|
+
*
|
|
32
|
+
* // With custom fallback
|
|
33
|
+
* const ObjectGrid = createLazyPlugin(
|
|
34
|
+
* () => import('@object-ui/plugin-grid'),
|
|
35
|
+
* { fallback: <div>Loading grid...</div> }
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createLazyPlugin<P extends object = any>(
|
|
40
|
+
importFn: () => Promise<{ default: React.ComponentType<P> }>,
|
|
41
|
+
options?: LazyPluginOptions
|
|
42
|
+
): React.ComponentType<P> {
|
|
43
|
+
const LazyComponent = lazy(importFn);
|
|
44
|
+
|
|
45
|
+
const PluginWrapper: React.FC<P> = (props) => (
|
|
46
|
+
<Suspense fallback={options?.fallback || null}>
|
|
47
|
+
<LazyComponent {...props} />
|
|
48
|
+
</Suspense>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return PluginWrapper;
|
|
52
|
+
}
|
package/src/SchemaRenderer.tsx
CHANGED
|
@@ -6,33 +6,85 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { forwardRef } from 'react';
|
|
10
|
-
import { SchemaNode, ComponentRegistry } from '@object-ui/core';
|
|
9
|
+
import React, { forwardRef, useContext, useMemo } from 'react';
|
|
10
|
+
import { SchemaNode, ComponentRegistry, ExpressionEvaluator } from '@object-ui/core';
|
|
11
|
+
import { SchemaRendererContext } from './context/SchemaRendererContext';
|
|
11
12
|
|
|
12
13
|
export const SchemaRenderer = forwardRef<any, { schema: SchemaNode } & Record<string, any>>(({ schema, ...props }, _ref) => {
|
|
13
|
-
|
|
14
|
+
const context = useContext(SchemaRendererContext);
|
|
15
|
+
const dataSource = context?.dataSource || {};
|
|
16
|
+
|
|
17
|
+
// Evaluate schema expressions against the data source
|
|
18
|
+
const evaluatedSchema = useMemo(() => {
|
|
19
|
+
if (!schema || typeof schema === 'string') return schema;
|
|
20
|
+
|
|
21
|
+
const evaluator = new ExpressionEvaluator({ data: dataSource });
|
|
22
|
+
// Shallow copy
|
|
23
|
+
const newSchema = { ...schema };
|
|
24
|
+
|
|
25
|
+
// COMPAT: Hoist 'properties' up to schema level
|
|
26
|
+
// This allows support for strict configs that wrap all props in 'properties'
|
|
27
|
+
if (newSchema.properties) {
|
|
28
|
+
Object.assign(newSchema, newSchema.properties);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Evaluate 'content' (common in Text, Button)
|
|
32
|
+
if (typeof newSchema.content === 'string') {
|
|
33
|
+
newSchema.content = evaluator.evaluate(newSchema.content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Evaluate 'props'
|
|
37
|
+
if (newSchema.props) {
|
|
38
|
+
const newProps = { ...newSchema.props };
|
|
39
|
+
for (const [key, val] of Object.entries(newProps)) {
|
|
40
|
+
newProps[key] = evaluator.evaluate(val as any);
|
|
41
|
+
}
|
|
42
|
+
newSchema.props = newProps;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return newSchema;
|
|
46
|
+
}, [schema, dataSource]);
|
|
47
|
+
|
|
48
|
+
if (!evaluatedSchema) return null;
|
|
14
49
|
// If schema is just a string, render it as text
|
|
15
|
-
if (typeof
|
|
50
|
+
if (typeof evaluatedSchema === 'string') return <>{evaluatedSchema}</>;
|
|
16
51
|
|
|
17
|
-
const Component = ComponentRegistry.get(
|
|
52
|
+
const Component = ComponentRegistry.get(evaluatedSchema.type);
|
|
18
53
|
|
|
19
54
|
if (!Component) {
|
|
20
55
|
return (
|
|
21
56
|
<div className="p-4 border border-red-500 rounded text-red-500 bg-red-50 my-2">
|
|
22
|
-
Unknown component type: <strong>{
|
|
23
|
-
<pre className="text-xs mt-2 overflow-auto">{JSON.stringify(
|
|
57
|
+
Unknown component type: <strong>{evaluatedSchema.type}</strong>
|
|
58
|
+
<pre className="text-xs mt-2 overflow-auto">{JSON.stringify(evaluatedSchema, null, 2)}</pre>
|
|
24
59
|
</div>
|
|
25
60
|
);
|
|
26
61
|
}
|
|
27
62
|
|
|
28
63
|
// Note: We don't forward the ref to the Component because components in the registry
|
|
29
64
|
// may not support refs. The SchemaRenderer itself can still receive refs for its own use.
|
|
65
|
+
|
|
66
|
+
// Extract schema metadata properties that should NOT be passed as React props
|
|
67
|
+
const {
|
|
68
|
+
type: _type,
|
|
69
|
+
children: _children,
|
|
70
|
+
body: _body,
|
|
71
|
+
schema: _schema,
|
|
72
|
+
visible: _visible,
|
|
73
|
+
visibleOn: _visibleOn,
|
|
74
|
+
hidden: _hidden,
|
|
75
|
+
hiddenOn: _hiddenOn,
|
|
76
|
+
disabled: _disabled,
|
|
77
|
+
disabledOn: _disabledOn,
|
|
78
|
+
...componentProps
|
|
79
|
+
} = evaluatedSchema;
|
|
80
|
+
|
|
30
81
|
return React.createElement(Component, {
|
|
31
|
-
schema,
|
|
32
|
-
...
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'data-obj-
|
|
82
|
+
schema: evaluatedSchema,
|
|
83
|
+
...componentProps, // Spread non-metadata schema properties as props
|
|
84
|
+
...(evaluatedSchema.props || {}), // Override with explicit props if provided
|
|
85
|
+
className: evaluatedSchema.className,
|
|
86
|
+
'data-obj-id': evaluatedSchema.id,
|
|
87
|
+
'data-obj-type': evaluatedSchema.type,
|
|
36
88
|
...props
|
|
37
89
|
});
|
|
38
90
|
});
|
|
@@ -0,0 +1,80 @@
|
|
|
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 { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { createLazyPlugin } from '../LazyPluginLoader';
|
|
13
|
+
|
|
14
|
+
describe('createLazyPlugin', () => {
|
|
15
|
+
it('should create a lazy-loaded component', async () => {
|
|
16
|
+
const TestComponent = () => <div>Test Component</div>;
|
|
17
|
+
|
|
18
|
+
const LazyComponent = createLazyPlugin(
|
|
19
|
+
() => Promise.resolve({ default: TestComponent })
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
render(<LazyComponent />);
|
|
23
|
+
|
|
24
|
+
await waitFor(() => {
|
|
25
|
+
expect(screen.getByText('Test Component')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should show custom fallback while loading', async () => {
|
|
30
|
+
const TestComponent = () => <div>Test Component</div>;
|
|
31
|
+
|
|
32
|
+
const LazyComponent = createLazyPlugin(
|
|
33
|
+
() => new Promise<{ default: React.ComponentType }>((resolve) => {
|
|
34
|
+
setTimeout(() => resolve({ default: TestComponent }), 100);
|
|
35
|
+
}),
|
|
36
|
+
{ fallback: <div>Loading...</div> }
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
render(<LazyComponent />);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
42
|
+
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(screen.getByText('Test Component')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should pass props to the lazy component', async () => {
|
|
49
|
+
interface TestProps {
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const TestComponent = ({ message }: TestProps) => <div>{message}</div>;
|
|
54
|
+
|
|
55
|
+
const LazyComponent = createLazyPlugin<TestProps>(
|
|
56
|
+
() => Promise.resolve({ default: TestComponent })
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
render(<LazyComponent message="Hello World" />);
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle loading errors gracefully', async () => {
|
|
67
|
+
// Mock console.error to avoid noise in test output
|
|
68
|
+
const originalError = console.error;
|
|
69
|
+
console.error = vi.fn();
|
|
70
|
+
|
|
71
|
+
const LazyComponent = createLazyPlugin<any>(
|
|
72
|
+
() => Promise.reject(new Error('Load error'))
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(() => render(<LazyComponent />)).not.toThrow();
|
|
76
|
+
|
|
77
|
+
// Restore console.error
|
|
78
|
+
console.error = originalError;
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -7,6 +7,8 @@ interface SchemaRendererContextType {
|
|
|
7
7
|
|
|
8
8
|
const SchemaRendererContext = createContext<SchemaRendererContextType | null>(null);
|
|
9
9
|
|
|
10
|
+
export { SchemaRendererContext };
|
|
11
|
+
|
|
10
12
|
export const SchemaRendererProvider = ({
|
|
11
13
|
children,
|
|
12
14
|
dataSource,
|
|
@@ -33,8 +35,9 @@ export const useSchemaContext = () => {
|
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
export const useDataScope = (path?: string) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
+
const context = useContext(SchemaRendererContext);
|
|
39
|
+
const dataSource = context?.dataSource;
|
|
40
|
+
if (!dataSource || !path) return dataSource;
|
|
38
41
|
// Simple path resolution for now. In real app might be more complex
|
|
39
42
|
return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
|
|
40
43
|
}
|