@object-ui/react 0.3.0 → 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.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/dist/LazyPluginLoader.d.ts +38 -0
  4. package/dist/LazyPluginLoader.js +35 -0
  5. package/dist/SchemaRenderer.d.ts +7 -0
  6. package/dist/SchemaRenderer.js +51 -12
  7. package/dist/__tests__/LazyPluginLoader.test.d.ts +8 -0
  8. package/dist/__tests__/LazyPluginLoader.test.js +49 -0
  9. package/dist/components/form/FieldFactory.d.ts +43 -0
  10. package/dist/components/form/FieldFactory.js +157 -0
  11. package/dist/components/form/FormRenderer.d.ts +40 -0
  12. package/dist/components/form/FormRenderer.js +87 -0
  13. package/dist/components/form/FormRenderer.test.d.ts +8 -0
  14. package/dist/components/form/FormRenderer.test.js +534 -0
  15. package/dist/components/form/index.d.ts +11 -0
  16. package/dist/components/form/index.js +9 -0
  17. package/dist/context/SchemaRendererContext.d.ts +14 -0
  18. package/dist/context/SchemaRendererContext.js +23 -0
  19. package/dist/context/index.d.ts +8 -1
  20. package/dist/context/index.js +8 -1
  21. package/dist/hooks/index.d.ts +9 -1
  22. package/dist/hooks/index.js +9 -1
  23. package/dist/hooks/useActionRunner.d.ts +32 -0
  24. package/dist/hooks/useActionRunner.js +65 -0
  25. package/dist/hooks/useExpression.d.ts +27 -0
  26. package/dist/hooks/useExpression.js +41 -0
  27. package/dist/index.d.ts +9 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.test.d.ts +7 -0
  30. package/dist/index.test.js +7 -0
  31. package/package.json +26 -6
  32. package/src/LazyPluginLoader.tsx +52 -0
  33. package/src/SchemaRenderer.tsx +72 -12
  34. package/src/__tests__/LazyPluginLoader.test.tsx +80 -0
  35. package/src/components/form/FieldFactory.tsx +504 -0
  36. package/src/components/form/FormRenderer.test.tsx +599 -0
  37. package/src/components/form/FormRenderer.tsx +209 -0
  38. package/src/components/form/README.md +352 -0
  39. package/src/components/form/index.ts +12 -0
  40. package/src/context/SchemaRendererContext.tsx +43 -0
  41. package/src/context/index.ts +9 -1
  42. package/src/hooks/index.ts +11 -1
  43. package/src/hooks/useActionRunner.ts +81 -0
  44. package/src/hooks/useExpression.ts +56 -0
  45. package/src/index.test.ts +8 -0
  46. package/src/index.ts +10 -0
  47. package/tsconfig.tsbuildinfo +1 -1
  48. package/vitest.config.ts +2 -0
@@ -0,0 +1,209 @@
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 from 'react';
10
+ import { useForm, FormProvider, UseFormReturn } from 'react-hook-form';
11
+ import type { FormView, FormSection } from '@objectstack/spec/ui';
12
+ import { FieldFactory } from './FieldFactory';
13
+
14
+ export interface FormRendererProps {
15
+ /**
16
+ * The FormView schema from @objectstack/spec
17
+ */
18
+ schema: FormView;
19
+ /**
20
+ * Initial form data
21
+ */
22
+ data?: Record<string, any>;
23
+ /**
24
+ * Form submission handler
25
+ */
26
+ onSubmit?: (data: Record<string, any>) => void | Promise<void>;
27
+ /**
28
+ * Form change handler
29
+ */
30
+ onChange?: (data: Record<string, any>) => void;
31
+ /**
32
+ * Custom class name for the form
33
+ */
34
+ className?: string;
35
+ /**
36
+ * Whether the form is disabled
37
+ */
38
+ disabled?: boolean;
39
+ }
40
+
41
+ /**
42
+ * FormRenderer component that renders forms based on FormViewSchema
43
+ * from @objectstack/spec
44
+ */
45
+ export const FormRenderer: React.FC<FormRendererProps> = ({
46
+ schema,
47
+ data = {},
48
+ onSubmit,
49
+ onChange,
50
+ className = '',
51
+ disabled = false,
52
+ }) => {
53
+ // Create form methods with react-hook-form
54
+ const methods = useForm({
55
+ defaultValues: data,
56
+ mode: 'onChange',
57
+ });
58
+
59
+ const { handleSubmit, watch } = methods;
60
+
61
+ // Watch for form changes
62
+ React.useEffect(() => {
63
+ if (onChange) {
64
+ const subscription = watch((value) => {
65
+ onChange(value as Record<string, any>);
66
+ });
67
+ return () => subscription.unsubscribe();
68
+ }
69
+ }, [watch, onChange]);
70
+
71
+ // Handle form submission
72
+ const onSubmitForm = handleSubmit(async (formData) => {
73
+ if (onSubmit) {
74
+ await onSubmit(formData);
75
+ }
76
+ });
77
+
78
+ // Render sections or fields
79
+ const renderContent = () => {
80
+ if (!schema.sections || schema.sections.length === 0) {
81
+ return null;
82
+ }
83
+
84
+ return (
85
+ <div className="space-y-6">
86
+ {schema.sections.map((section, index) => (
87
+ <FormSectionRenderer
88
+ key={index}
89
+ section={section}
90
+ methods={methods}
91
+ disabled={disabled}
92
+ />
93
+ ))}
94
+ </div>
95
+ );
96
+ };
97
+
98
+ return (
99
+ <FormProvider {...methods}>
100
+ <form onSubmit={onSubmitForm} className={`space-y-6 ${className}`}>
101
+ {renderContent()}
102
+
103
+ {/* Submit button - optional, can be customized */}
104
+ <div className="flex justify-end gap-2">
105
+ <button
106
+ type="submit"
107
+ disabled={disabled}
108
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
109
+ >
110
+ Submit
111
+ </button>
112
+ </div>
113
+ </form>
114
+ </FormProvider>
115
+ );
116
+ };
117
+
118
+ interface FormSectionRendererProps {
119
+ section: FormSection;
120
+ methods: UseFormReturn<any>;
121
+ disabled?: boolean;
122
+ }
123
+
124
+ // Grid column classes for different column counts
125
+ const GRID_COLS = {
126
+ 1: 'grid-cols-1',
127
+ 2: 'grid-cols-1 md:grid-cols-2',
128
+ 3: 'grid-cols-1 md:grid-cols-3',
129
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
130
+ } as const;
131
+
132
+ // Column span classes
133
+ const COL_SPAN = {
134
+ 1: 'col-span-1',
135
+ 2: 'col-span-2',
136
+ 3: 'col-span-3',
137
+ 4: 'col-span-4',
138
+ } as const;
139
+
140
+ /**
141
+ * Renders a form section with grid layout
142
+ */
143
+ const FormSectionRenderer: React.FC<FormSectionRendererProps> = ({
144
+ section,
145
+ methods,
146
+ disabled = false,
147
+ }) => {
148
+ const [isCollapsed, setIsCollapsed] = React.useState(section.collapsed || false);
149
+
150
+ // Determine grid columns based on section.columns
151
+ const gridCols = GRID_COLS[section.columns || 1];
152
+
153
+ const handleToggleCollapse = () => {
154
+ if (section.collapsible) {
155
+ setIsCollapsed(!isCollapsed);
156
+ }
157
+ };
158
+
159
+ return (
160
+ <div className="border rounded-lg p-4">
161
+ {section.label && (
162
+ <div
163
+ className={`flex items-center justify-between mb-4 ${
164
+ section.collapsible ? 'cursor-pointer' : ''
165
+ }`}
166
+ onClick={handleToggleCollapse}
167
+ >
168
+ <h3 className="text-lg font-semibold">{section.label}</h3>
169
+ {section.collapsible && (
170
+ <span className="text-sm text-gray-500">
171
+ {isCollapsed ? '▶' : '▼'}
172
+ </span>
173
+ )}
174
+ </div>
175
+ )}
176
+
177
+ {!isCollapsed && (
178
+ <div className={`grid ${gridCols} gap-4`}>
179
+ {section.fields.map((field, index) => {
180
+ // Handle both string fields (legacy) and FormFieldSchema objects
181
+ const fieldName = typeof field === 'string' ? field : field.field;
182
+ const fieldConfig = typeof field === 'string' ? { field: fieldName } : field;
183
+
184
+ // Skip hidden fields
185
+ if (fieldConfig.hidden) {
186
+ return null;
187
+ }
188
+
189
+ // Calculate colSpan for grid
190
+ const colSpan = fieldConfig.colSpan || 1;
191
+ const colSpanClass = COL_SPAN[Math.min(colSpan, section.columns || 1) as keyof typeof COL_SPAN];
192
+
193
+ return (
194
+ <div key={`${fieldName}-${index}`} className={colSpanClass}>
195
+ <FieldFactory
196
+ field={fieldConfig}
197
+ methods={methods}
198
+ disabled={disabled || fieldConfig.readonly}
199
+ />
200
+ </div>
201
+ );
202
+ })}
203
+ </div>
204
+ )}
205
+ </div>
206
+ );
207
+ };
208
+
209
+ FormRenderer.displayName = 'FormRenderer';
@@ -0,0 +1,352 @@
1
+ # FormRenderer Component
2
+
3
+ The `FormRenderer` component is a React component that renders forms based on the `FormView` schema from `@objectstack/spec`. It provides a flexible and powerful way to create forms with minimal code.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Schema-driven**: Define forms using JSON schema from `@objectstack/spec`
8
+ - ✅ **Multi-column layouts**: Support for 1-4 column grid layouts
9
+ - ✅ **Collapsible sections**: Organize forms with collapsible sections
10
+ - ✅ **Form state management**: Built-in state management using `react-hook-form`
11
+ - ✅ **Multiple field types**: Text, Number, Checkbox, Textarea, Select, Date, DateTime, Time
12
+ - ⏳ **Conditional fields**: Show/hide fields based on conditions (planned)
13
+ - ⏳ **Schema validation**: Zod validation support (planned)
14
+ - ✅ **Column spanning**: Fields can span multiple columns
15
+ - ✅ **Help text**: Display helpful descriptions for fields
16
+ - ✅ **Required fields**: Mark fields as required with validation
17
+
18
+ ## Installation
19
+
20
+ The FormRenderer component is part of the `@object-ui/react` package:
21
+
22
+ ```bash
23
+ npm install @object-ui/react @objectstack/spec react-hook-form
24
+ ```
25
+
26
+ Or with pnpm:
27
+
28
+ ```bash
29
+ pnpm add @object-ui/react @objectstack/spec react-hook-form
30
+ ```
31
+
32
+ ## Basic Usage
33
+
34
+ ```tsx
35
+ import { FormRenderer } from '@object-ui/react';
36
+ import type { FormView } from '@objectstack/spec/ui';
37
+
38
+ const schema: FormView = {
39
+ type: 'simple',
40
+ sections: [
41
+ {
42
+ label: 'Contact Information',
43
+ collapsible: false,
44
+ collapsed: false,
45
+ columns: 2,
46
+ fields: [
47
+ {
48
+ field: 'firstName',
49
+ label: 'First Name',
50
+ required: true,
51
+ widget: 'text',
52
+ },
53
+ {
54
+ field: 'lastName',
55
+ label: 'Last Name',
56
+ required: true,
57
+ widget: 'text',
58
+ },
59
+ {
60
+ field: 'email',
61
+ label: 'Email',
62
+ required: true,
63
+ widget: 'email',
64
+ helpText: 'We will never share your email',
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ };
70
+
71
+ function MyForm() {
72
+ const handleSubmit = (data: Record<string, any>) => {
73
+ console.log('Form submitted:', data);
74
+ };
75
+
76
+ return (
77
+ <FormRenderer
78
+ schema={schema}
79
+ onSubmit={handleSubmit}
80
+ />
81
+ );
82
+ }
83
+ ```
84
+
85
+ ## Props
86
+
87
+ ### FormRendererProps
88
+
89
+ | Prop | Type | Required | Description |
90
+ |------|------|----------|-------------|
91
+ | `schema` | `FormView` | Yes | The FormView schema from @objectstack/spec |
92
+ | `data` | `Record<string, any>` | No | Initial form data |
93
+ | `onSubmit` | `(data: Record<string, any>) => void \| Promise<void>` | No | Form submission handler |
94
+ | `onChange` | `(data: Record<string, any>) => void` | No | Form change handler |
95
+ | `className` | `string` | No | Custom CSS class for the form |
96
+ | `disabled` | `boolean` | No | Whether the form is disabled |
97
+
98
+ ## Schema Structure
99
+
100
+ ### FormView
101
+
102
+ The main schema that defines the entire form.
103
+
104
+ ```typescript
105
+ interface FormView {
106
+ type: 'simple' | 'tabbed' | 'wizard';
107
+ sections?: FormSection[];
108
+ data?: DataSource; // Optional data source configuration
109
+ }
110
+ ```
111
+
112
+ ### FormSection
113
+
114
+ Defines a section of the form with its own layout.
115
+
116
+ ```typescript
117
+ interface FormSection {
118
+ label?: string;
119
+ collapsible?: boolean;
120
+ collapsed?: boolean;
121
+ columns: 1 | 2 | 3 | 4;
122
+ fields: (string | FormField)[];
123
+ }
124
+ ```
125
+
126
+ ### FormField
127
+
128
+ Defines individual form fields.
129
+
130
+ ```typescript
131
+ interface FormField {
132
+ field: string; // Field name
133
+ label?: string;
134
+ placeholder?: string;
135
+ helpText?: string;
136
+ readonly?: boolean;
137
+ required?: boolean;
138
+ hidden?: boolean;
139
+ colSpan?: number; // Number of columns to span (1-4)
140
+ widget?: string; // Widget type (text, number, checkbox, etc.)
141
+ dependsOn?: string; // Field dependency
142
+ visibleOn?: string; // Visibility condition
143
+ }
144
+ ```
145
+
146
+ ## Supported Widget Types
147
+
148
+ The `FieldFactory` component supports the following widget types:
149
+
150
+ ### Text Input Types
151
+ - `text`, `string` - Basic text input
152
+ - `email` - Email input with validation
153
+ - `password` - Password input (masked)
154
+ - `url` - URL input
155
+ - `tel`, `phone` - Telephone/phone number input
156
+ - `textarea` - Multi-line text input
157
+ - `markdown` - Markdown text editor (textarea with monospace font)
158
+ - `html` - HTML/Rich text editor (textarea with monospace font)
159
+
160
+ ### Numeric Input Types
161
+ - `number`, `integer`, `float` - Number input
162
+ - `currency` - Currency input with $ prefix
163
+ - `percent` - Percentage input with % suffix
164
+
165
+ ### Boolean & Selection Types
166
+ - `checkbox`, `boolean` - Checkbox input
167
+ - `select`, `dropdown` - Select dropdown
168
+
169
+ ### Date & Time Types
170
+ - `date` - Date picker
171
+ - `datetime`, `datetime-local` - DateTime picker
172
+ - `time` - Time picker
173
+
174
+ ### File Upload Types
175
+ - `file` - File upload input
176
+ - `image` - Image upload input (defaults to image/* mime types)
177
+
178
+ ### Relationship Types
179
+ - `lookup` - Lookup/reference field (select with options)
180
+ - `master_detail` - Master-detail relationship (select with options)
181
+ - `user`, `owner` - User selection field (select with options)
182
+
183
+ ### Complex & Special Types
184
+ - `location` - Geographic location (latitude/longitude inputs)
185
+ - `object` - JSON object editor (textarea with JSON validation)
186
+ - `formula` - Read-only computed field
187
+ - `summary` - Read-only aggregation/rollup field
188
+ - `auto_number` - Read-only auto-generated number
189
+ - `vector` - Vector/embedding display (read-only)
190
+ - `grid` - Sub-table/grid editor (placeholder)
191
+
192
+ ## Extended Field Properties
193
+
194
+ Some field types support additional properties beyond the base FormField schema:
195
+
196
+ ```typescript
197
+ interface ExtendedFormField extends FormField {
198
+ // For file/image uploads
199
+ multiple?: boolean;
200
+ accept?: string[];
201
+
202
+ // For select/lookup/user fields
203
+ options?: Array<{
204
+ label: string;
205
+ value: string;
206
+ disabled?: boolean;
207
+ }>;
208
+ }
209
+ ```
210
+
211
+ Example usage with extended properties:
212
+
213
+ ```tsx
214
+ {
215
+ field: 'avatar',
216
+ label: 'Profile Picture',
217
+ widget: 'image',
218
+ accept: ['image/png', 'image/jpeg'],
219
+ multiple: false
220
+ }
221
+
222
+ {
223
+ field: 'account',
224
+ label: 'Account',
225
+ widget: 'lookup',
226
+ options: [
227
+ { label: 'Account 1', value: '1' },
228
+ { label: 'Account 2', value: '2' }
229
+ ]
230
+ }
231
+ ```
232
+
233
+ ## Known Limitations
234
+
235
+ ### Version 0.3.0
236
+
237
+ - **Conditional Visibility**: The `visibleOn` and `dependsOn` properties are not yet implemented. Fields are visible by default unless explicitly marked as `hidden`.
238
+ - **Advanced Select Options**: While select/lookup/user fields support options via the `ExtendedFormField` interface, they don't yet support advanced features like search, async loading, or custom rendering.
239
+ - **Rich Text Editors**: The `markdown` and `html` widgets currently use plain textareas with monospace fonts. Full WYSIWYG editors are planned for future releases.
240
+ - **File Upload Preview**: File and image widgets support uploads but don't yet show previews or provide upload progress indicators.
241
+ - **Location Maps**: The location widget provides lat/lng inputs but doesn't include map visualization or geocoding.
242
+ - **Grid Editor**: The grid widget is a placeholder and doesn't yet provide spreadsheet-like editing capabilities.
243
+ - **Validation**: Currently only basic required field validation is supported via react-hook-form. Advanced Zod schema validation is planned for future releases.
244
+
245
+ These features are planned for future releases.
246
+
247
+ ## Examples
248
+
249
+ ### Multi-Section Form
250
+
251
+ ```tsx
252
+ const schema: FormView = {
253
+ type: 'simple',
254
+ sections: [
255
+ {
256
+ label: 'Personal Information',
257
+ collapsible: true,
258
+ collapsed: false,
259
+ columns: 2,
260
+ fields: [
261
+ { field: 'firstName', label: 'First Name', required: true, widget: 'text' },
262
+ { field: 'lastName', label: 'Last Name', required: true, widget: 'text' },
263
+ { field: 'email', label: 'Email', required: true, widget: 'email' },
264
+ { field: 'phone', label: 'Phone', widget: 'tel' },
265
+ ],
266
+ },
267
+ {
268
+ label: 'Preferences',
269
+ collapsible: true,
270
+ collapsed: true,
271
+ columns: 1,
272
+ fields: [
273
+ {
274
+ field: 'newsletter',
275
+ label: 'Subscribe to newsletter',
276
+ widget: 'checkbox',
277
+ helpText: 'Receive updates about our products',
278
+ },
279
+ ],
280
+ },
281
+ ],
282
+ };
283
+ ```
284
+
285
+ ### Form with Initial Data
286
+
287
+ ```tsx
288
+ const initialData = {
289
+ firstName: 'John',
290
+ lastName: 'Doe',
291
+ email: 'john@example.com',
292
+ newsletter: true,
293
+ };
294
+
295
+ <FormRenderer
296
+ schema={schema}
297
+ data={initialData}
298
+ onSubmit={handleSubmit}
299
+ />
300
+ ```
301
+
302
+ ### Form with Column Spanning
303
+
304
+ ```tsx
305
+ const schema: FormView = {
306
+ type: 'simple',
307
+ sections: [
308
+ {
309
+ label: 'Contact',
310
+ columns: 2,
311
+ fields: [
312
+ { field: 'name', label: 'Name', widget: 'text' },
313
+ { field: 'email', label: 'Email', widget: 'email' },
314
+ {
315
+ field: 'message',
316
+ label: 'Message',
317
+ widget: 'textarea',
318
+ colSpan: 2, // Spans both columns
319
+ },
320
+ ],
321
+ },
322
+ ],
323
+ };
324
+ ```
325
+
326
+ ## Architecture
327
+
328
+ The FormRenderer is built on top of:
329
+
330
+ - **react-hook-form**: For form state management and validation
331
+ - **@objectstack/spec**: For schema definitions
332
+
333
+ ## Customization
334
+
335
+ You can customize the appearance of the form by:
336
+
337
+ 1. Using the `className` prop on the FormRenderer
338
+ 2. Using Tailwind CSS classes in your application
339
+ 3. Extending the FieldFactory component for custom widgets
340
+
341
+ ## Related Components
342
+
343
+ - `FieldFactory`: Renders individual form fields based on widget type
344
+ - `SchemaRenderer`: General-purpose schema renderer for ObjectUI
345
+
346
+ ## License
347
+
348
+ MIT
349
+
350
+ ## Contributing
351
+
352
+ Contributions are welcome! Please read the [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
@@ -0,0 +1,12 @@
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
+ export { FormRenderer } from './FormRenderer';
10
+ export type { FormRendererProps } from './FormRenderer';
11
+ export { FieldFactory } from './FieldFactory';
12
+ export type { FieldFactoryProps, ExtendedFormField } from './FieldFactory';
@@ -0,0 +1,43 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+
3
+ interface SchemaRendererContextType {
4
+ dataSource: any;
5
+ debug?: boolean;
6
+ }
7
+
8
+ const SchemaRendererContext = createContext<SchemaRendererContextType | null>(null);
9
+
10
+ export { SchemaRendererContext };
11
+
12
+ export const SchemaRendererProvider = ({
13
+ children,
14
+ dataSource,
15
+ debug
16
+ }: {
17
+ children: React.ReactNode;
18
+ dataSource: any;
19
+ debug?: boolean
20
+ }) => {
21
+ const value = useMemo(() => ({ dataSource, debug }), [dataSource, debug]);
22
+ return (
23
+ <SchemaRendererContext.Provider value={value}>
24
+ {children}
25
+ </SchemaRendererContext.Provider>
26
+ );
27
+ };
28
+
29
+ export const useSchemaContext = () => {
30
+ const context = useContext(SchemaRendererContext);
31
+ if (!context) {
32
+ throw new Error('useSchemaContext must be used within a SchemaRendererProvider');
33
+ }
34
+ return context;
35
+ };
36
+
37
+ export const useDataScope = (path?: string) => {
38
+ const context = useContext(SchemaRendererContext);
39
+ const dataSource = context?.dataSource;
40
+ if (!dataSource || !path) return dataSource;
41
+ // Simple path resolution for now. In real app might be more complex
42
+ return path.split('.').reduce((acc, part) => acc && acc[part], dataSource);
43
+ }
@@ -1 +1,9 @@
1
- export {};
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
+ export * from './SchemaRendererContext';
@@ -1 +1,11 @@
1
- export {};
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
+ export * from './useExpression';
10
+ export * from './useActionRunner';
11
+