diff --git a/packages/plugin-object/src/ObjectForm.tsx b/packages/plugin-object/src/ObjectForm.tsx index e3dcc9cf..dabea10b 100644 --- a/packages/plugin-object/src/ObjectForm.tsx +++ b/packages/plugin-object/src/ObjectForm.tsx @@ -128,18 +128,57 @@ export const ObjectForm: React.FC = ({ }; // Add field-specific properties - if (field.type === 'select' || field.type === 'lookup') { + if (field.type === 'select' || field.type === 'lookup' || field.type === 'master_detail') { formField.options = field.options || []; + formField.multiple = field.multiple; } - if (field.type === 'number' || field.type === 'currency') { + if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') { formField.min = field.min; formField.max = field.max; formField.step = field.precision ? Math.pow(10, -field.precision) : undefined; } - if (field.type === 'text' || field.type === 'textarea') { - formField.maxLength = field.maxLength; + if (field.type === 'text' || field.type === 'textarea' || field.type === 'markdown' || field.type === 'html') { + formField.maxLength = field.max_length; + formField.minLength = field.min_length; + } + + if (field.type === 'file' || field.type === 'image') { + formField.multiple = field.multiple; + formField.accept = field.accept ? field.accept.join(',') : undefined; + // Add validation hints for file size and dimensions + if (field.max_size) { + const sizeHint = `Max size: ${formatFileSize(field.max_size)}`; + formField.description = formField.description + ? `${formField.description} (${sizeHint})` + : sizeHint; + } + } + + if (field.type === 'email') { + formField.inputType = 'email'; + } + + if (field.type === 'phone') { + formField.inputType = 'tel'; + } + + if (field.type === 'url') { + formField.inputType = 'url'; + } + + if (field.type === 'password') { + formField.inputType = 'password'; + } + + if (field.type === 'time') { + formField.inputType = 'time'; + } + + // Read-only fields for computed types + if (field.type === 'formula' || field.type === 'summary' || field.type === 'auto_number') { + formField.disabled = true; } generatedFields.push(formField); @@ -245,6 +284,12 @@ export const ObjectForm: React.FC = ({ * `select`). If a field type is not explicitly mapped, the function falls * back to the generic `"input"` type. * + * Updated to support all field types from @objectql/types v3.0.1: + * text, textarea, markdown, html, select, date, datetime, time, number, + * currency, percent, boolean, email, phone, url, image, file, location, + * lookup, master_detail, password, formula, summary, auto_number, object, + * vector, grid + * * @param fieldType - The ObjectQL field type identifier to convert * (for example: `"text"`, `"number"`, `"date"`, `"lookup"`). * @returns The normalized form field type string used in the form schema @@ -252,22 +297,79 @@ export const ObjectForm: React.FC = ({ */ function mapFieldTypeToFormType(fieldType: string): string { const typeMap: Record = { + // Text-based fields text: 'input', textarea: 'textarea', + markdown: 'textarea', // Markdown editor (fallback to textarea) + html: 'textarea', // Rich text editor (fallback to textarea) + + // Numeric fields number: 'input', currency: 'input', percent: 'input', + + // Date/Time fields date: 'date-picker', datetime: 'date-picker', + time: 'input', // Time picker (fallback to input with type="time") + + // Boolean boolean: 'switch', + + // Selection fields select: 'select', + lookup: 'select', + master_detail: 'select', + + // Contact fields email: 'input', + phone: 'input', url: 'input', + + // File fields + file: 'file-upload', + image: 'file-upload', + + // Special fields password: 'input', - lookup: 'select', - master_detail: 'select', - fileupload: 'file-upload', + location: 'input', // Location/map field (fallback to input) + + // Auto-generated/computed fields (typically read-only) + formula: 'input', + summary: 'input', + auto_number: 'input', + + // Complex data types + object: 'input', // JSON object (fallback to input) + vector: 'input', // Vector/embedding data (fallback to input) + grid: 'input', // Grid/table data (fallback to input) }; return typeMap[fieldType] || 'input'; } + +/** + * Formats file size in bytes to human-readable string + * @param bytes - File size in bytes (must be non-negative) + * @returns Formatted string (e.g., "5 MB", "1.5 GB") + */ +function formatFileSize(bytes: number): string { + if (bytes < 0 || !Number.isFinite(bytes)) { + return '0 B'; + } + + if (bytes === 0) { + return '0 B'; + } + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`; +} diff --git a/packages/plugin-object/src/ObjectTable.tsx b/packages/plugin-object/src/ObjectTable.tsx index ac80b451..6609a250 100644 --- a/packages/plugin-object/src/ObjectTable.tsx +++ b/packages/plugin-object/src/ObjectTable.tsx @@ -96,6 +96,46 @@ export const ObjectTable: React.FC = ({ accessorKey: fieldName, }; + // Add field type-specific formatting hints + if (field.type === 'date' || field.type === 'datetime') { + column.type = 'date'; + } else if (field.type === 'boolean') { + column.type = 'boolean'; + } else if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') { + column.type = 'number'; + } else if (field.type === 'image' || field.type === 'file') { + // For file/image fields, display the name or count + column.cell = (value: any) => { + if (!value) return '-'; + if (Array.isArray(value)) { + const count = value.length; + const fileType = field.type === 'image' ? 'image' : 'file'; + return count === 1 ? `1 ${fileType}` : `${count} ${fileType}s`; + } + return value.name || value.original_name || 'File'; + }; + } else if (field.type === 'lookup' || field.type === 'master_detail') { + // For relationship fields, display the name property if available + column.cell = (value: any) => { + if (!value) return '-'; + if (typeof value === 'object' && value !== null) { + // Try common display properties first + if (value.name) return value.name; + if (value.label) return value.label; + if (value._id) return value._id; + // Fallback to object type indicator + return '[Object]'; + } + return String(value); + }; + } else if (field.type === 'url') { + // For URL fields, make them clickable + column.cell = (value: any) => { + if (!value) return '-'; + return value; // The table renderer should handle URL formatting + }; + } + generatedColumns.push(column); } });