@object-ui/react 0.3.1 → 2.0.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/CHANGELOG.md +13 -0
- package/dist/LazyPluginLoader.d.ts +82 -0
- package/dist/LazyPluginLoader.js +124 -0
- package/dist/SchemaRenderer.js +44 -12
- package/dist/components/form/FieldFactory.js +44 -40
- package/dist/components/form/FormRenderer.js +45 -3
- package/dist/context/ActionContext.d.ts +76 -0
- package/dist/context/ActionContext.js +139 -0
- package/dist/context/SchemaRendererContext.d.ts +2 -1
- package/dist/context/SchemaRendererContext.js +4 -2
- package/dist/context/ThemeContext.d.ts +94 -0
- package/dist/context/ThemeContext.js +215 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useActionRunner.d.ts +21 -7
- package/dist/hooks/useActionRunner.js +27 -7
- package/dist/hooks/useDiscovery.d.ts +78 -0
- package/dist/hooks/useDiscovery.js +96 -0
- package/dist/hooks/useDynamicApp.d.ts +70 -0
- package/dist/hooks/useDynamicApp.js +92 -0
- package/dist/hooks/useNavigationOverlay.d.ts +78 -0
- package/dist/hooks/useNavigationOverlay.js +112 -0
- package/dist/hooks/usePageVariables.d.ts +67 -0
- package/dist/hooks/usePageVariables.js +109 -0
- package/dist/hooks/useViewData.d.ts +73 -0
- package/dist/hooks/useViewData.js +135 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/utils/i18n.d.ts +10 -0
- package/dist/utils/i18n.js +12 -0
- package/package.json +16 -6
- package/src/LazyPluginLoader.tsx +174 -0
- package/src/SchemaRenderer.tsx +64 -12
- package/src/__tests__/LazyPluginLoader.test.tsx +176 -0
- package/src/__tests__/ThemeProvider.test.tsx +451 -0
- package/src/__tests__/useNavigationOverlay.test.ts +416 -0
- package/src/__tests__/usePageVariables.test.tsx +334 -0
- package/src/__tests__/useViewData.test.tsx +422 -0
- package/src/components/form/FieldFactory.tsx +51 -46
- package/src/components/form/FormRenderer.test.tsx +145 -0
- package/src/components/form/FormRenderer.tsx +52 -3
- package/src/context/ActionContext.tsx +213 -0
- package/src/context/SchemaRendererContext.tsx +5 -2
- package/src/context/ThemeContext.tsx +325 -0
- package/src/context/__tests__/ActionContext.test.tsx +174 -0
- package/src/context/index.ts +2 -0
- package/src/hooks/__tests__/useActionRunner.test.ts +153 -0
- package/src/hooks/__tests__/useDiscovery.test.tsx +149 -0
- package/src/hooks/__tests__/useDynamicApp.test.ts +161 -0
- package/src/hooks/__tests__/useExpression.test.ts +86 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useActionRunner.ts +49 -11
- package/src/hooks/useDiscovery.ts +141 -0
- package/src/hooks/useDynamicApp.ts +142 -0
- package/src/hooks/useNavigationOverlay.ts +180 -0
- package/src/hooks/usePageVariables.tsx +157 -0
- package/src/hooks/useViewData.ts +201 -0
- package/src/index.ts +22 -0
- package/src/utils/i18n.ts +10 -0
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
- package/dist/components/form/FormRenderer.test.d.ts +0 -8
- package/dist/components/form/FormRenderer.test.js +0 -534
- package/dist/index.test.d.ts +0 -8
- package/dist/index.test.js +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @object-ui/react
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- b859617: Release v1.0.0 — unify all package versions to 1.0.0
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [b859617]
|
|
12
|
+
- @object-ui/types@2.0.0
|
|
13
|
+
- @object-ui/core@2.0.0
|
|
14
|
+
- @object-ui/i18n@2.0.0
|
|
15
|
+
|
|
3
16
|
## 0.3.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* Number of retry attempts on load failure (default: 2)
|
|
16
|
+
*/
|
|
17
|
+
retries?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Delay in ms between retries (default: 1000)
|
|
20
|
+
*/
|
|
21
|
+
retryDelay?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Custom error fallback component
|
|
24
|
+
*/
|
|
25
|
+
errorFallback?: React.ComponentType<{
|
|
26
|
+
error: Error;
|
|
27
|
+
retry: () => void;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
32
|
+
*
|
|
33
|
+
* @param importFn - Dynamic import function that returns a module with default export
|
|
34
|
+
* @param options - Configuration options for the lazy plugin
|
|
35
|
+
* @returns A component that lazy loads the plugin
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // Basic usage
|
|
40
|
+
* const ObjectGrid = createLazyPlugin(
|
|
41
|
+
* () => import('@object-ui/plugin-grid')
|
|
42
|
+
* );
|
|
43
|
+
*
|
|
44
|
+
* // With custom fallback
|
|
45
|
+
* const ObjectGrid = createLazyPlugin(
|
|
46
|
+
* () => import('@object-ui/plugin-grid'),
|
|
47
|
+
* { fallback: <div>Loading grid...</div> }
|
|
48
|
+
* );
|
|
49
|
+
*
|
|
50
|
+
* // With retry and error handling
|
|
51
|
+
* const ObjectGrid = createLazyPlugin(
|
|
52
|
+
* () => import('@object-ui/plugin-grid'),
|
|
53
|
+
* {
|
|
54
|
+
* retries: 3,
|
|
55
|
+
* errorFallback: ({ error, retry }) => (
|
|
56
|
+
* <div>
|
|
57
|
+
* <p>Failed to load: {error.message}</p>
|
|
58
|
+
* <button onClick={retry}>Retry</button>
|
|
59
|
+
* </div>
|
|
60
|
+
* ),
|
|
61
|
+
* }
|
|
62
|
+
* );
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function createLazyPlugin<P extends object = any>(importFn: () => Promise<{
|
|
66
|
+
default: React.ComponentType<P>;
|
|
67
|
+
}>, options?: LazyPluginOptions): React.ComponentType<P>;
|
|
68
|
+
/**
|
|
69
|
+
* Preload a plugin module without rendering it.
|
|
70
|
+
* Useful for preloading plugins that will be needed soon.
|
|
71
|
+
*
|
|
72
|
+
* @param importFn - Dynamic import function
|
|
73
|
+
* @returns Promise that resolves when the module is loaded
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* // Preload on hover
|
|
78
|
+
* const loadGrid = () => import('@object-ui/plugin-grid');
|
|
79
|
+
* <button onMouseEnter={() => preloadPlugin(loadGrid)}>Show Grid</button>
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function preloadPlugin<T = unknown>(importFn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,124 @@
|
|
|
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, Component } from 'react';
|
|
10
|
+
class PluginErrorBoundary extends Component {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
Object.defineProperty(this, "state", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: { hasError: false, error: null }
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "handleRetry", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: () => {
|
|
24
|
+
this.setState({ hasError: false, error: null });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
static getDerivedStateFromError(error) {
|
|
29
|
+
return { hasError: true, error };
|
|
30
|
+
}
|
|
31
|
+
render() {
|
|
32
|
+
if (this.state.hasError && this.state.error) {
|
|
33
|
+
const FallbackComponent = this.props.fallback;
|
|
34
|
+
if (FallbackComponent) {
|
|
35
|
+
return _jsx(FallbackComponent, { error: this.state.error, retry: this.handleRetry });
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return this.props.children;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a lazy-loaded import function with retry support
|
|
44
|
+
*/
|
|
45
|
+
function createRetryImport(importFn, retries, retryDelay) {
|
|
46
|
+
return () => {
|
|
47
|
+
let attempt = 0;
|
|
48
|
+
const tryImport = () => importFn().catch((err) => {
|
|
49
|
+
attempt++;
|
|
50
|
+
if (attempt <= retries) {
|
|
51
|
+
return new Promise((resolve) => setTimeout(() => resolve(tryImport()), retryDelay));
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
});
|
|
55
|
+
return tryImport();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a lazy-loaded plugin component with Suspense wrapper
|
|
60
|
+
*
|
|
61
|
+
* @param importFn - Dynamic import function that returns a module with default export
|
|
62
|
+
* @param options - Configuration options for the lazy plugin
|
|
63
|
+
* @returns A component that lazy loads the plugin
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```tsx
|
|
67
|
+
* // Basic usage
|
|
68
|
+
* const ObjectGrid = createLazyPlugin(
|
|
69
|
+
* () => import('@object-ui/plugin-grid')
|
|
70
|
+
* );
|
|
71
|
+
*
|
|
72
|
+
* // With custom fallback
|
|
73
|
+
* const ObjectGrid = createLazyPlugin(
|
|
74
|
+
* () => import('@object-ui/plugin-grid'),
|
|
75
|
+
* { fallback: <div>Loading grid...</div> }
|
|
76
|
+
* );
|
|
77
|
+
*
|
|
78
|
+
* // With retry and error handling
|
|
79
|
+
* const ObjectGrid = createLazyPlugin(
|
|
80
|
+
* () => import('@object-ui/plugin-grid'),
|
|
81
|
+
* {
|
|
82
|
+
* retries: 3,
|
|
83
|
+
* errorFallback: ({ error, retry }) => (
|
|
84
|
+
* <div>
|
|
85
|
+
* <p>Failed to load: {error.message}</p>
|
|
86
|
+
* <button onClick={retry}>Retry</button>
|
|
87
|
+
* </div>
|
|
88
|
+
* ),
|
|
89
|
+
* }
|
|
90
|
+
* );
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function createLazyPlugin(importFn, options) {
|
|
94
|
+
const { retries = 2, retryDelay = 1000, errorFallback } = options || {};
|
|
95
|
+
const retryImport = retries > 0
|
|
96
|
+
? createRetryImport(importFn, retries, retryDelay)
|
|
97
|
+
: importFn;
|
|
98
|
+
const LazyComponent = lazy(retryImport);
|
|
99
|
+
const PluginWrapper = (props) => {
|
|
100
|
+
const content = (_jsx(Suspense, { fallback: options?.fallback || null, children: _jsx(LazyComponent, { ...props }) }));
|
|
101
|
+
if (errorFallback) {
|
|
102
|
+
return (_jsx(PluginErrorBoundary, { fallback: errorFallback, children: content }));
|
|
103
|
+
}
|
|
104
|
+
return content;
|
|
105
|
+
};
|
|
106
|
+
return PluginWrapper;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Preload a plugin module without rendering it.
|
|
110
|
+
* Useful for preloading plugins that will be needed soon.
|
|
111
|
+
*
|
|
112
|
+
* @param importFn - Dynamic import function
|
|
113
|
+
* @returns Promise that resolves when the module is loaded
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* // Preload on hover
|
|
118
|
+
* const loadGrid = () => import('@object-ui/plugin-grid');
|
|
119
|
+
* <button onMouseEnter={() => preloadPlugin(loadGrid)}>Show Grid</button>
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function preloadPlugin(importFn) {
|
|
123
|
+
return importFn();
|
|
124
|
+
}
|
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
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveI18nLabel } from '../../utils/i18n';
|
|
2
3
|
/**
|
|
3
4
|
* FieldFactory component that renders different input types based on
|
|
4
5
|
* the widget property or field type
|
|
@@ -11,6 +12,10 @@ export const FieldFactory = ({ field, methods, disabled = false, }) => {
|
|
|
11
12
|
const widgetType = field.widget || 'text';
|
|
12
13
|
const fieldName = field.field;
|
|
13
14
|
const error = errors[fieldName];
|
|
15
|
+
// Resolve I18nLabel values to plain strings
|
|
16
|
+
const fieldLabel = resolveI18nLabel(field.label);
|
|
17
|
+
const fieldPlaceholder = resolveI18nLabel(field.placeholder);
|
|
18
|
+
const fieldHelpText = resolveI18nLabel(field.helpText);
|
|
14
19
|
// Helper function to handle multiple select value conversion
|
|
15
20
|
const handleMultipleSelectValue = (value) => {
|
|
16
21
|
if (extendedField.multiple && value instanceof HTMLCollection) {
|
|
@@ -19,14 +24,13 @@ export const FieldFactory = ({ field, methods, disabled = false, }) => {
|
|
|
19
24
|
return value;
|
|
20
25
|
};
|
|
21
26
|
// Handle conditional visibility
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// Skip if explicitly hidden
|
|
27
|
+
// Fields are hidden when explicitly hidden or when visibleOn evaluates to false
|
|
28
|
+
// Note: dependsOn is handled at the FormSectionRenderer level
|
|
25
29
|
if (field.hidden) {
|
|
26
30
|
return null;
|
|
27
31
|
}
|
|
28
32
|
// Common field wrapper
|
|
29
|
-
const renderField = (input) => (_jsxs("div", { className: "space-y-2", children: [
|
|
33
|
+
const renderField = (input) => (_jsxs("div", { className: "space-y-2", children: [fieldLabel && (_jsxs("label", { htmlFor: fieldName, className: "block text-sm font-medium text-gray-700", children: [fieldLabel, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), input, fieldHelpText && (_jsx("p", { className: "text-sm text-gray-500", children: fieldHelpText })), error && (_jsx("p", { className: "text-sm text-red-600", children: error.message }))] }));
|
|
30
34
|
// Render based on widget type
|
|
31
35
|
switch (widgetType.toLowerCase()) {
|
|
32
36
|
case 'text':
|
|
@@ -35,71 +39,71 @@ export const FieldFactory = ({ field, methods, disabled = false, }) => {
|
|
|
35
39
|
case 'password':
|
|
36
40
|
case 'url':
|
|
37
41
|
case 'tel':
|
|
38
|
-
return renderField(_jsx("input", { id: fieldName, type: widgetType === 'string' ? 'text' : widgetType, placeholder:
|
|
39
|
-
required: field.required ? `${
|
|
42
|
+
return renderField(_jsx("input", { id: fieldName, type: widgetType === 'string' ? 'text' : widgetType, placeholder: fieldPlaceholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
43
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
40
44
|
}) }));
|
|
41
45
|
case 'number':
|
|
42
46
|
case 'integer':
|
|
43
47
|
case 'float':
|
|
44
|
-
return renderField(_jsx("input", { id: fieldName, type: "number", placeholder:
|
|
45
|
-
required: field.required ? `${
|
|
48
|
+
return renderField(_jsx("input", { id: fieldName, type: "number", placeholder: fieldPlaceholder, disabled: disabled, step: widgetType === 'integer' ? '1' : 'any', className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
49
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
46
50
|
valueAsNumber: true,
|
|
47
51
|
}) }));
|
|
48
52
|
case 'checkbox':
|
|
49
53
|
case 'boolean':
|
|
50
|
-
return (_jsxs("div", { className: "flex items-start space-x-2", children: [_jsx("input", { id: fieldName, type: "checkbox", disabled: disabled, className: "mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 disabled:opacity-50", ...register(fieldName) }), _jsxs("div", { className: "flex-1", children: [
|
|
54
|
+
return (_jsxs("div", { className: "flex items-start space-x-2", children: [_jsx("input", { id: fieldName, type: "checkbox", disabled: disabled, className: "mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 disabled:opacity-50", ...register(fieldName) }), _jsxs("div", { className: "flex-1", children: [fieldLabel && (_jsxs("label", { htmlFor: fieldName, className: "text-sm font-medium text-gray-700", children: [fieldLabel, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), fieldHelpText && (_jsx("p", { className: "text-sm text-gray-500 mt-1", children: fieldHelpText })), error && (_jsx("p", { className: "text-sm text-red-600 mt-1", children: error.message }))] })] }));
|
|
51
55
|
case 'textarea':
|
|
52
|
-
return renderField(_jsx("textarea", { id: fieldName, placeholder:
|
|
53
|
-
required: field.required ? `${
|
|
56
|
+
return renderField(_jsx("textarea", { id: fieldName, placeholder: fieldPlaceholder, disabled: disabled, rows: 4, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
57
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
54
58
|
}) }));
|
|
55
59
|
case 'select':
|
|
56
60
|
case 'dropdown':
|
|
57
61
|
return renderField(_jsxs("select", { id: fieldName, disabled: disabled, multiple: extendedField.multiple, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
58
|
-
required: field.required ? `${
|
|
62
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
59
63
|
setValueAs: handleMultipleSelectValue,
|
|
60
|
-
}), children: [!extendedField.multiple && _jsx("option", { value: "", children:
|
|
64
|
+
}), children: [!extendedField.multiple && _jsx("option", { value: "", children: fieldPlaceholder || 'Select an option' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
|
|
61
65
|
case 'date':
|
|
62
|
-
return renderField(_jsx("input", { id: fieldName, type: "date", placeholder:
|
|
63
|
-
required: field.required ? `${
|
|
66
|
+
return renderField(_jsx("input", { id: fieldName, type: "date", placeholder: fieldPlaceholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
67
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
64
68
|
}) }));
|
|
65
69
|
case 'datetime':
|
|
66
70
|
case 'datetime-local':
|
|
67
|
-
return renderField(_jsx("input", { id: fieldName, type: "datetime-local", placeholder:
|
|
68
|
-
required: field.required ? `${
|
|
71
|
+
return renderField(_jsx("input", { id: fieldName, type: "datetime-local", placeholder: fieldPlaceholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
72
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
69
73
|
}) }));
|
|
70
74
|
case 'time':
|
|
71
|
-
return renderField(_jsx("input", { id: fieldName, type: "time", placeholder:
|
|
72
|
-
required: field.required ? `${
|
|
75
|
+
return renderField(_jsx("input", { id: fieldName, type: "time", placeholder: fieldPlaceholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
76
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
73
77
|
}) }));
|
|
74
78
|
case 'currency':
|
|
75
|
-
return renderField(_jsxs("div", { className: "relative", children: [_jsx("span", { className: "absolute left-3 top-2 text-gray-500", children: "$" }), _jsx("input", { id: fieldName, type: "number", placeholder:
|
|
76
|
-
required: field.required ? `${
|
|
79
|
+
return renderField(_jsxs("div", { className: "relative", children: [_jsx("span", { className: "absolute left-3 top-2 text-gray-500", children: "$" }), _jsx("input", { id: fieldName, type: "number", placeholder: fieldPlaceholder, disabled: disabled, step: "0.01", className: "w-full pl-8 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
80
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
77
81
|
valueAsNumber: true,
|
|
78
82
|
}) })] }));
|
|
79
83
|
case 'percent':
|
|
80
|
-
return renderField(_jsxs("div", { className: "relative", children: [_jsx("input", { id: fieldName, type: "number", placeholder:
|
|
81
|
-
required: field.required ? `${
|
|
84
|
+
return renderField(_jsxs("div", { className: "relative", children: [_jsx("input", { id: fieldName, type: "number", placeholder: fieldPlaceholder, disabled: disabled, step: "0.01", min: "0", max: "100", className: "w-full pr-8 pl-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
85
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
82
86
|
valueAsNumber: true,
|
|
83
87
|
}) }), _jsx("span", { className: "absolute right-3 top-2 text-gray-500", children: "%" })] }));
|
|
84
88
|
case 'phone':
|
|
85
|
-
return renderField(_jsx("input", { id: fieldName, type: "tel", placeholder:
|
|
86
|
-
required: field.required ? `${
|
|
89
|
+
return renderField(_jsx("input", { id: fieldName, type: "tel", placeholder: fieldPlaceholder || '+1 (555) 000-0000', disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
90
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
87
91
|
}) }));
|
|
88
92
|
case 'markdown':
|
|
89
|
-
return renderField(_jsx("textarea", { id: fieldName, placeholder:
|
|
90
|
-
required: field.required ? `${
|
|
93
|
+
return renderField(_jsx("textarea", { id: fieldName, placeholder: fieldPlaceholder || 'Enter markdown text...', disabled: disabled, rows: 8, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono", ...register(fieldName, {
|
|
94
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
91
95
|
}) }));
|
|
92
96
|
case 'html':
|
|
93
|
-
return renderField(_jsx("textarea", { id: fieldName, placeholder:
|
|
94
|
-
required: field.required ? `${
|
|
97
|
+
return renderField(_jsx("textarea", { id: fieldName, placeholder: fieldPlaceholder || 'Enter HTML...', disabled: disabled, rows: 8, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono", ...register(fieldName, {
|
|
98
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
95
99
|
}) }));
|
|
96
100
|
case 'file':
|
|
97
101
|
return renderField(_jsx("input", { id: fieldName, type: "file", disabled: disabled, multiple: extendedField.multiple, accept: extendedField.accept?.join(','), className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100", ...register(fieldName, {
|
|
98
|
-
required: field.required ? `${
|
|
102
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
99
103
|
}) }));
|
|
100
104
|
case 'image':
|
|
101
105
|
return renderField(_jsx("input", { id: fieldName, type: "file", disabled: disabled, multiple: extendedField.multiple, accept: extendedField.accept?.join(',') || 'image/*', className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100", ...register(fieldName, {
|
|
102
|
-
required: field.required ? `${
|
|
106
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
103
107
|
}) }));
|
|
104
108
|
case 'location':
|
|
105
109
|
return renderField(_jsxs("div", { className: "space-y-2", children: [_jsx("input", { id: `${fieldName}-lat`, type: "number", placeholder: "Latitude", disabled: disabled, step: "any", className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(`${fieldName}.lat`, {
|
|
@@ -112,23 +116,23 @@ export const FieldFactory = ({ field, methods, disabled = false, }) => {
|
|
|
112
116
|
case 'lookup':
|
|
113
117
|
case 'master_detail':
|
|
114
118
|
return renderField(_jsxs("select", { id: fieldName, disabled: disabled, multiple: extendedField.multiple, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
115
|
-
required: field.required ? `${
|
|
119
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
116
120
|
setValueAs: handleMultipleSelectValue,
|
|
117
|
-
}), children: [!extendedField.multiple && _jsx("option", { value: "", children:
|
|
121
|
+
}), children: [!extendedField.multiple && _jsx("option", { value: "", children: fieldPlaceholder || 'Select an option' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
|
|
118
122
|
case 'user':
|
|
119
123
|
case 'owner':
|
|
120
124
|
return renderField(_jsxs("select", { id: fieldName, disabled: disabled, multiple: extendedField.multiple, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
121
|
-
required: field.required ? `${
|
|
125
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
122
126
|
setValueAs: handleMultipleSelectValue,
|
|
123
|
-
}), children: [!extendedField.multiple && _jsx("option", { value: "", children:
|
|
127
|
+
}), children: [!extendedField.multiple && _jsx("option", { value: "", children: fieldPlaceholder || 'Select user' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
|
|
124
128
|
case 'formula':
|
|
125
129
|
case 'summary':
|
|
126
130
|
case 'auto_number':
|
|
127
131
|
// Read-only computed fields - display as disabled text input
|
|
128
132
|
return renderField(_jsx("input", { id: fieldName, type: "text", disabled: true, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 cursor-not-allowed text-gray-600", ...register(fieldName) }));
|
|
129
133
|
case 'object':
|
|
130
|
-
return renderField(_jsx("textarea", { id: fieldName, placeholder:
|
|
131
|
-
required: field.required ? `${
|
|
134
|
+
return renderField(_jsx("textarea", { id: fieldName, placeholder: fieldPlaceholder || 'Enter JSON object...', disabled: disabled, rows: 6, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono text-sm", ...register(fieldName, {
|
|
135
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
132
136
|
validate: (value) => {
|
|
133
137
|
if (!value)
|
|
134
138
|
return true;
|
|
@@ -143,14 +147,14 @@ export const FieldFactory = ({ field, methods, disabled = false, }) => {
|
|
|
143
147
|
}) }));
|
|
144
148
|
case 'vector':
|
|
145
149
|
// Vector fields are typically read-only or require specialized input
|
|
146
|
-
return renderField(_jsx("input", { id: fieldName, type: "text", placeholder:
|
|
150
|
+
return renderField(_jsx("input", { id: fieldName, type: "text", placeholder: fieldPlaceholder || 'Vector data (read-only)', disabled: true, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 cursor-not-allowed text-gray-600", ...register(fieldName) }));
|
|
147
151
|
case 'grid':
|
|
148
152
|
// Grid fields require complex table/grid editor - placeholder for now
|
|
149
153
|
return renderField(_jsx("div", { className: "w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-600", children: _jsx("p", { className: "text-sm", children: "Grid editor not yet implemented" }) }));
|
|
150
154
|
default:
|
|
151
155
|
// Default to text input
|
|
152
|
-
return renderField(_jsx("input", { id: fieldName, type: "text", placeholder:
|
|
153
|
-
required: field.required ? `${
|
|
156
|
+
return renderField(_jsx("input", { id: fieldName, type: "text", placeholder: fieldPlaceholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
|
|
157
|
+
required: field.required ? `${fieldLabel || fieldName} is required` : false,
|
|
154
158
|
}) }));
|
|
155
159
|
}
|
|
156
160
|
};
|
|
@@ -9,6 +9,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { useForm, FormProvider } from 'react-hook-form';
|
|
11
11
|
import { FieldFactory } from './FieldFactory';
|
|
12
|
+
import { ExpressionEvaluator } from '@object-ui/core';
|
|
13
|
+
import { resolveI18nLabel } from '../../utils/i18n';
|
|
12
14
|
/**
|
|
13
15
|
* FormRenderer component that renders forms based on FormViewSchema
|
|
14
16
|
* from @objectstack/spec
|
|
@@ -58,6 +60,33 @@ const COL_SPAN = {
|
|
|
58
60
|
3: 'col-span-3',
|
|
59
61
|
4: 'col-span-4',
|
|
60
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Evaluate whether a field should be visible based on visibleOn expression
|
|
65
|
+
* and dependsOn parent field value.
|
|
66
|
+
*/
|
|
67
|
+
function evaluateFieldVisibility(fieldConfig, formValues) {
|
|
68
|
+
// If explicitly hidden, skip
|
|
69
|
+
if (fieldConfig.hidden) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Evaluate visibleOn expression
|
|
73
|
+
if (fieldConfig.visibleOn) {
|
|
74
|
+
try {
|
|
75
|
+
const evaluator = new ExpressionEvaluator({ data: formValues, form: formValues });
|
|
76
|
+
return evaluator.evaluateCondition(fieldConfig.visibleOn);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// On error, default to visible
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Evaluate dependsOn: field is hidden if parent field has no value
|
|
84
|
+
if (fieldConfig.dependsOn) {
|
|
85
|
+
const parentValue = formValues[fieldConfig.dependsOn];
|
|
86
|
+
return parentValue !== undefined && parentValue !== null && parentValue !== '';
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
61
90
|
/**
|
|
62
91
|
* Renders a form section with grid layout
|
|
63
92
|
*/
|
|
@@ -65,17 +94,30 @@ const FormSectionRenderer = ({ section, methods, disabled = false, }) => {
|
|
|
65
94
|
const [isCollapsed, setIsCollapsed] = React.useState(section.collapsed || false);
|
|
66
95
|
// Determine grid columns based on section.columns
|
|
67
96
|
const gridCols = GRID_COLS[section.columns || 1];
|
|
97
|
+
// Collect whether there are any conditional fields
|
|
98
|
+
const hasConditionalFields = React.useMemo(() => {
|
|
99
|
+
return section.fields.some((field) => {
|
|
100
|
+
if (typeof field === 'string')
|
|
101
|
+
return false;
|
|
102
|
+
return !!field.dependsOn || !!field.visibleOn;
|
|
103
|
+
});
|
|
104
|
+
}, [section.fields]);
|
|
105
|
+
// Watch form values for conditional field evaluation
|
|
106
|
+
// Only watch when there are fields with dependsOn or visibleOn to avoid
|
|
107
|
+
// unnecessary re-renders in forms without conditional fields.
|
|
108
|
+
const allFormValues = methods.watch();
|
|
109
|
+
const formValues = hasConditionalFields ? allFormValues : undefined;
|
|
68
110
|
const handleToggleCollapse = () => {
|
|
69
111
|
if (section.collapsible) {
|
|
70
112
|
setIsCollapsed(!isCollapsed);
|
|
71
113
|
}
|
|
72
114
|
};
|
|
73
|
-
return (_jsxs("div", { className: "border rounded-lg p-4", children: [section.label && (_jsxs("div", { className: `flex items-center justify-between mb-4 ${section.collapsible ? 'cursor-pointer' : ''}`, onClick: handleToggleCollapse, children: [_jsx("h3", { className: "text-lg font-semibold", children: section.label }), section.collapsible && (_jsx("span", { className: "text-sm text-gray-500", children: isCollapsed ? '▶' : '▼' }))] })), !isCollapsed && (_jsx("div", { className: `grid ${gridCols} gap-4`, children: section.fields.map((field, index) => {
|
|
115
|
+
return (_jsxs("div", { className: "border rounded-lg p-4", children: [section.label && (_jsxs("div", { className: `flex items-center justify-between mb-4 ${section.collapsible ? 'cursor-pointer' : ''}`, onClick: handleToggleCollapse, children: [_jsx("h3", { className: "text-lg font-semibold", children: resolveI18nLabel(section.label) }), section.collapsible && (_jsx("span", { className: "text-sm text-gray-500", children: isCollapsed ? '▶' : '▼' }))] })), !isCollapsed && (_jsx("div", { className: `grid ${gridCols} gap-4`, children: section.fields.map((field, index) => {
|
|
74
116
|
// Handle both string fields (legacy) and FormFieldSchema objects
|
|
75
117
|
const fieldName = typeof field === 'string' ? field : field.field;
|
|
76
118
|
const fieldConfig = typeof field === 'string' ? { field: fieldName } : field;
|
|
77
|
-
//
|
|
78
|
-
if (fieldConfig
|
|
119
|
+
// Evaluate visibility: static hidden, visibleOn expression, dependsOn parent
|
|
120
|
+
if (!evaluateFieldVisibility(fieldConfig, formValues || {})) {
|
|
79
121
|
return null;
|
|
80
122
|
}
|
|
81
123
|
// Calculate colSpan for grid
|