Skip to content

Commit 30b41da

Browse files
committed
Enhance ObjectForm and field components to support specialized field widgets and improve integration tests
1 parent c7bcc6f commit 30b41da

File tree

7 files changed

+114
-35
lines changed

7 files changed

+114
-35
lines changed

examples/msw-object-form/src/__tests__/ObjectFormTypeMapping.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,20 @@ describe('ObjectForm Field Type Mapping', () => {
100100
const selectInWrapper = selectWrapper?.querySelector('button[role="combobox"]');
101101
expect(selectInWrapper).toBeInTheDocument();
102102

103-
// 6. Currency -> Input[type=number]
103+
// 6. Currency -> CurrencyField (which has $ prefix)
104+
// If CurrencyField is working, it renders a wrapping div with '$' span and Input[type=number]
104105
// Debug currency field rendering
105106
const currencyInput = container.querySelector('input[name="currency_field"]');
106107
if (!currencyInput) {
107108
console.log('Currency field not found in DOM:', document.body.innerHTML);
108109
}
109110
expect(currencyInput).toBeInTheDocument();
110-
expect(currencyInput).toHaveAttribute('type', 'number');
111+
expect(currencyInput).toHaveAttribute('type', 'number'); // CurrencyField uses number input
112+
113+
// Extended Verification: Check for CurrencyField specific UI
114+
// CurrencyField adds a '$' or currency symbol span
115+
const currencySymbol = screen.getByText('$');
116+
expect(currencySymbol).toBeInTheDocument();
111117

112118
// 7. Percent -> Input
113119
const percentInput = container.querySelector('input[name="percent_field"]');

packages/components/src/renderers/form/form.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,16 @@ interface RenderFieldProps {
401401
}
402402

403403
function renderFieldComponent(type: string, props: RenderFieldProps) {
404+
// 1. Try to resolve specialized field widget from registry first
405+
// We prioritize registry components (e.g., 'field.currency', 'field.date')
406+
const RegisteredComponent = ComponentRegistry.get(type);
407+
408+
if (RegisteredComponent) {
409+
// For specialized fields (e.g. fields package), they expect 'field' prop.
410+
// Ensure we pass all props.
411+
return <RegisteredComponent {...props} />;
412+
}
413+
404414
const { inputType, options = [], placeholder, ...fieldProps } = props;
405415

406416
switch (type) {

packages/fields/src/index.tsx

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import React from 'react';
1010
import type { FieldMetadata, SelectOptionMetadata } from '@object-ui/types';
11+
import { ComponentRegistry } from '@object-ui/core';
1112
import { Badge, Avatar, AvatarFallback, Button } from '@object-ui/components';
1213
import { Check, X } from 'lucide-react';
1314

@@ -519,54 +520,49 @@ registerFieldRenderer('select', SelectCellRenderer);
519520
export function mapFieldTypeToFormType(fieldType: string): string {
520521
const typeMap: Record<string, string> = {
521522
// Text-based fields
522-
text: 'input',
523-
textarea: 'textarea',
524-
markdown: 'textarea', // Markdown editor (fallback to textarea)
525-
html: 'textarea', // Rich text editor (fallback to textarea)
523+
text: 'field:text',
524+
textarea: 'field:textarea',
525+
markdown: 'field:markdown', // Markdown editor (fallback to textarea)
526+
html: 'field:html', // Rich text editor (fallback to textarea)
526527

527528
// Numeric fields
528-
number: 'input',
529-
currency: 'input',
530-
percent: 'input',
529+
number: 'field:number',
530+
currency: 'field:currency',
531+
percent: 'field:percent',
531532

532533
// Date/Time fields
533-
date: 'date-picker',
534-
datetime: 'date-picker',
535-
time: 'input', // Time picker (fallback to input with type="time")
534+
date: 'field:date',
535+
datetime: 'field:datetime',
536+
time: 'field:time',
536537

537538
// Boolean
538-
boolean: 'switch',
539+
boolean: 'field:boolean',
539540

540541
// Selection fields
541-
select: 'select',
542-
lookup: 'select',
543-
master_detail: 'select',
542+
select: 'field:select',
543+
lookup: 'field:lookup',
544+
master_detail: 'field:master_detail',
544545

545546
// Contact fields
546-
email: 'input',
547-
phone: 'input',
548-
url: 'input',
547+
email: 'field:email',
548+
phone: 'field:phone',
549+
url: 'field:url',
549550

550551
// File fields
551-
file: 'file-upload',
552-
image: 'file-upload',
552+
file: 'field:file',
553+
image: 'field:image',
553554

554555
// Special fields
555-
password: 'input',
556-
location: 'input', // Location/map field (fallback to input)
556+
password: 'field:password',
557+
location: 'field:location', // Location/map field (fallback to input)
557558

558559
// Auto-generated/computed fields (typically read-only)
559-
formula: 'input',
560-
summary: 'input',
561-
auto_number: 'input',
562-
563-
// Complex data types
564-
object: 'input', // JSON object (fallback to input)
565-
vector: 'input', // Vector/embedding data (fallback to input)
566-
grid: 'input', // Grid/table data (fallback to input)
560+
formula: 'field:formula',
561+
summary: 'field:summary',
562+
auto_number: 'field:auto_number',
567563
};
568564

569-
return typeMap[fieldType] || 'input';
565+
return typeMap[fieldType] || 'field:text';
570566
}
571567

572568
/**
@@ -890,9 +886,8 @@ export function registerField(fieldType: string): void {
890886
// Create lazy component
891887
const LazyFieldWidget = React.lazy(loader);
892888

893-
// Register with field namespace
894-
const renderer = createFieldRenderer(LazyFieldWidget);
895-
ComponentRegistry.register(fieldType, renderer, { namespace: 'field' });
889+
// Register with field namespace - NO WRAPPER to allow form renderer to control label/layout
890+
ComponentRegistry.register(fieldType, LazyFieldWidget, { namespace: 'field' });
896891
}
897892

898893
/**
@@ -1011,3 +1006,6 @@ export * from './widgets/GeolocationField';
10111006
export * from './widgets/SignatureField';
10121007
export * from './widgets/QRCodeField';
10131008
export * from './widgets/MasterDetailField';
1009+
1010+
// Initialize registry
1011+
registerAllFields();

packages/fields/src/widgets/CurrencyField.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export function CurrencyField({ value, onChange, field, readonly, errorMessage,
5656
className={`pl-8 ${props.className || ''}`}
5757
step={Math.pow(10, -precision).toFixed(precision)}
5858
aria-invalid={!!errorMessage}
59+
{...props}
5960
/>
6061
</div>
6162
);

packages/fields/src/widgets/TextField.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function TextField({ value, onChange, field, readonly, ...props }: FieldW
3131
placeholder={field.placeholder}
3232
disabled={readonly}
3333
className={props.className}
34+
{...props}
3435
/>
3536
);
3637
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import { ObjectForm } from './ObjectForm';
4+
import { registerAllFields } from '@object-ui/fields';
5+
import React from 'react';
6+
7+
// Ensure fields are registered
8+
registerAllFields();
9+
10+
describe('ObjectForm Integration', () => {
11+
const objectSchema = {
12+
name: 'test_object',
13+
fields: {
14+
name: {
15+
type: 'text',
16+
label: 'Name'
17+
},
18+
price: {
19+
type: 'currency',
20+
label: 'Price',
21+
scale: 2
22+
}
23+
}
24+
};
25+
26+
const mockDataSource: any = {
27+
getObjectSchema: vi.fn().mockResolvedValue(objectSchema),
28+
createRecord: vi.fn(),
29+
updateRecord: vi.fn(),
30+
getRecord: vi.fn(),
31+
query: vi.fn()
32+
};
33+
34+
it('renders fields using specialized components', async () => {
35+
render(
36+
<ObjectForm
37+
schema={{
38+
type: 'object-form',
39+
objectName: 'test_object',
40+
mode: 'create'
41+
}}
42+
dataSource={mockDataSource}
43+
/>
44+
);
45+
46+
// Wait for schema to load (useEffect)
47+
await waitFor(() => {
48+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('test_object');
49+
});
50+
51+
// Check if labels are present
52+
await waitFor(() => {
53+
expect(screen.queryByText('Name')).toBeTruthy();
54+
});
55+
expect(screen.getByText('Price')).toBeTruthy();
56+
57+
// Assert input exists
58+
// Since we don't have getByLabelText working reliably without full accessibility tree in happy-dom sometimes,
59+
// we can try looking for inputs.
60+
});
61+
});

packages/plugin-form/src/ObjectForm.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
178178
placeholder: field.placeholder,
179179
description: field.help || field.description,
180180
validation: buildValidationRules(field),
181+
// Important: Pass the original field metadata so widgets can access properties like precision, currency, etc.
182+
field: field,
181183
};
182184

183185
// Add field-specific properties

0 commit comments

Comments
 (0)