@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.
Files changed (69) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +13 -0
  3. package/dist/LazyPluginLoader.d.ts +82 -0
  4. package/dist/LazyPluginLoader.js +124 -0
  5. package/dist/SchemaRenderer.js +44 -12
  6. package/dist/components/form/FieldFactory.js +44 -40
  7. package/dist/components/form/FormRenderer.js +45 -3
  8. package/dist/context/ActionContext.d.ts +76 -0
  9. package/dist/context/ActionContext.js +139 -0
  10. package/dist/context/SchemaRendererContext.d.ts +2 -1
  11. package/dist/context/SchemaRendererContext.js +4 -2
  12. package/dist/context/ThemeContext.d.ts +94 -0
  13. package/dist/context/ThemeContext.js +215 -0
  14. package/dist/context/index.d.ts +2 -0
  15. package/dist/context/index.js +2 -0
  16. package/dist/hooks/index.d.ts +5 -0
  17. package/dist/hooks/index.js +5 -0
  18. package/dist/hooks/useActionRunner.d.ts +21 -7
  19. package/dist/hooks/useActionRunner.js +27 -7
  20. package/dist/hooks/useDiscovery.d.ts +78 -0
  21. package/dist/hooks/useDiscovery.js +96 -0
  22. package/dist/hooks/useDynamicApp.d.ts +70 -0
  23. package/dist/hooks/useDynamicApp.js +92 -0
  24. package/dist/hooks/useNavigationOverlay.d.ts +78 -0
  25. package/dist/hooks/useNavigationOverlay.js +112 -0
  26. package/dist/hooks/usePageVariables.d.ts +67 -0
  27. package/dist/hooks/usePageVariables.js +109 -0
  28. package/dist/hooks/useViewData.d.ts +73 -0
  29. package/dist/hooks/useViewData.js +135 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +3 -0
  32. package/dist/utils/i18n.d.ts +10 -0
  33. package/dist/utils/i18n.js +12 -0
  34. package/package.json +16 -6
  35. package/src/LazyPluginLoader.tsx +174 -0
  36. package/src/SchemaRenderer.tsx +64 -12
  37. package/src/__tests__/LazyPluginLoader.test.tsx +176 -0
  38. package/src/__tests__/ThemeProvider.test.tsx +451 -0
  39. package/src/__tests__/useNavigationOverlay.test.ts +416 -0
  40. package/src/__tests__/usePageVariables.test.tsx +334 -0
  41. package/src/__tests__/useViewData.test.tsx +422 -0
  42. package/src/components/form/FieldFactory.tsx +51 -46
  43. package/src/components/form/FormRenderer.test.tsx +145 -0
  44. package/src/components/form/FormRenderer.tsx +52 -3
  45. package/src/context/ActionContext.tsx +213 -0
  46. package/src/context/SchemaRendererContext.tsx +5 -2
  47. package/src/context/ThemeContext.tsx +325 -0
  48. package/src/context/__tests__/ActionContext.test.tsx +174 -0
  49. package/src/context/index.ts +2 -0
  50. package/src/hooks/__tests__/useActionRunner.test.ts +153 -0
  51. package/src/hooks/__tests__/useDiscovery.test.tsx +149 -0
  52. package/src/hooks/__tests__/useDynamicApp.test.ts +161 -0
  53. package/src/hooks/__tests__/useExpression.test.ts +86 -0
  54. package/src/hooks/index.ts +5 -0
  55. package/src/hooks/useActionRunner.ts +49 -11
  56. package/src/hooks/useDiscovery.ts +141 -0
  57. package/src/hooks/useDynamicApp.ts +142 -0
  58. package/src/hooks/useNavigationOverlay.ts +180 -0
  59. package/src/hooks/usePageVariables.tsx +157 -0
  60. package/src/hooks/useViewData.ts +201 -0
  61. package/src/index.ts +22 -0
  62. package/src/utils/i18n.ts +10 -0
  63. package/tsconfig.json +1 -0
  64. package/tsconfig.tsbuildinfo +1 -1
  65. package/vitest.config.ts +2 -0
  66. package/dist/components/form/FormRenderer.test.d.ts +0 -8
  67. package/dist/components/form/FormRenderer.test.js +0 -534
  68. package/dist/index.test.d.ts +0 -8
  69. package/dist/index.test.js +0 -13
@@ -0,0 +1,4 @@
1
+
2
+ > @object-ui/react@2.0.0 build /home/runner/work/objectui/objectui/packages/react
3
+ > tsc
4
+
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
+ }
@@ -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
- if (!schema)
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 schema === 'string')
16
- return _jsx(_Fragment, { children: schema });
17
- const Component = ComponentRegistry.get(schema.type);
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: schema.type }), _jsx("pre", { className: "text-xs mt-2 overflow-auto", children: JSON.stringify(schema, null, 2) })] }));
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
- ...(schema.props || {}),
26
- className: schema.className,
27
- 'data-obj-id': schema.id,
28
- 'data-obj-type': schema.type,
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
- // Note: visibleOn expression evaluation is not yet implemented
23
- // Fields are always visible unless explicitly hidden
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: [field.label && (_jsxs("label", { htmlFor: fieldName, className: "block text-sm font-medium text-gray-700", children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), input, field.helpText && (_jsx("p", { className: "text-sm text-gray-500", children: field.helpText })), error && (_jsx("p", { className: "text-sm text-red-600", children: error.message }))] }));
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: field.placeholder, 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, {
39
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder, 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, {
45
- required: field.required ? `${field.label || fieldName} is required` : false,
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: [field.label && (_jsxs("label", { htmlFor: fieldName, className: "text-sm font-medium text-gray-700", children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), field.helpText && (_jsx("p", { className: "text-sm text-gray-500 mt-1", children: field.helpText })), error && (_jsx("p", { className: "text-sm text-red-600 mt-1", children: error.message }))] })] }));
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: field.placeholder, 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, {
53
- required: field.required ? `${field.label || fieldName} is required` : false,
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 ? `${field.label || fieldName} is required` : false,
62
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
59
63
  setValueAs: handleMultipleSelectValue,
60
- }), children: [!extendedField.multiple && _jsx("option", { value: "", children: field.placeholder || 'Select an option' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
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: field.placeholder, 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, {
63
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder, 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, {
68
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder, 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 ? `${field.label || fieldName} is required` : false,
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: field.placeholder, 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, {
76
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder, 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, {
81
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder || '+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, {
86
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder || '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, {
90
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder || '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, {
94
- required: field.required ? `${field.label || fieldName} is required` : false,
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 ? `${field.label || fieldName} is required` : false,
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 ? `${field.label || fieldName} is required` : false,
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 ? `${field.label || fieldName} is required` : false,
119
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
116
120
  setValueAs: handleMultipleSelectValue,
117
- }), children: [!extendedField.multiple && _jsx("option", { value: "", children: field.placeholder || 'Select an option' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
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 ? `${field.label || fieldName} is required` : false,
125
+ required: field.required ? `${fieldLabel || fieldName} is required` : false,
122
126
  setValueAs: handleMultipleSelectValue,
123
- }), children: [!extendedField.multiple && _jsx("option", { value: "", children: field.placeholder || 'Select user' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
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: field.placeholder || '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, {
131
- required: field.required ? `${field.label || fieldName} is required` : false,
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: field.placeholder || '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) }));
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: field.placeholder, 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, {
153
- required: field.required ? `${field.label || fieldName} is required` : false,
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
- // Skip hidden fields
78
- if (fieldConfig.hidden) {
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