diff --git a/docs/plugins/plugin-object.mdx b/docs/plugins/plugin-object.mdx index 61e118786..afee8a5af 100644 --- a/docs/plugins/plugin-object.mdx +++ b/docs/plugins/plugin-object.mdx @@ -23,10 +23,10 @@ The Object plugin provides seamless integration with ObjectQL backends through s type: "object-table", objectName: "users", columns: [ - { key: "name", label: "Name" }, - { key: "email", label: "Email" }, - { key: "role", label: "Role" }, - { key: "status", label: "Status" } + { header: "Name", accessorKey: "name" }, + { header: "Email", accessorKey: "email" }, + { header: "Role", accessorKey: "role" }, + { header: "Status", accessorKey: "status" } ], data: [ { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Admin", status: "Active" }, @@ -45,7 +45,8 @@ The Object plugin provides seamless integration with ObjectQL backends through s schema={{ type: "object-form", objectName: "user", - fields: [ + mode: "create", + customFields: [ { name: "name", label: "Full Name", @@ -75,7 +76,7 @@ The Object plugin provides seamless integration with ObjectQL backends through s defaultValue: "Active" } ], - submitLabel: "Create User" + submitText: "Create User" }} title="ObjectForm Component" description="Smart form with validation and schema integration" diff --git a/packages/plugin-object/src/ObjectForm.tsx b/packages/plugin-object/src/ObjectForm.tsx index 0ec4cdb2c..b0d465a98 100644 --- a/packages/plugin-object/src/ObjectForm.tsx +++ b/packages/plugin-object/src/ObjectForm.tsx @@ -26,8 +26,9 @@ export interface ObjectFormProps { /** * ObjectQL data source + * Optional when using inline field definitions (customFields or fields array with field objects) */ - dataSource: ObjectQLDataSource; + dataSource?: ObjectQLDataSource; /** * Additional CSS class @@ -63,10 +64,24 @@ export const ObjectForm: React.FC = ({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - // Fetch object schema from ObjectQL + // Check if using inline fields (fields defined as objects, not just names) + const hasInlineFields = schema.customFields && schema.customFields.length > 0; + + // Initialize with inline data if provided + useEffect(() => { + if (hasInlineFields) { + setInitialData(schema.initialData || schema.initialValues || {}); + setLoading(false); + } + }, [hasInlineFields, schema.initialData, schema.initialValues]); + + // Fetch object schema from ObjectQL (skip if using inline fields) useEffect(() => { const fetchObjectSchema = async () => { try { + if (!dataSource) { + throw new Error('DataSource is required when using ObjectQL schema fetching (inline fields not provided)'); + } const schemaData = await dataSource.getObjectSchema(schema.objectName); setObjectSchema(schemaData); } catch (err) { @@ -75,16 +90,34 @@ export const ObjectForm: React.FC = ({ } }; - if (schema.objectName && dataSource) { + // Skip fetching if we have inline fields + if (hasInlineFields) { + // Use a minimal schema for inline fields + setObjectSchema({ + name: schema.objectName, + fields: {} as Record, + }); + } else if (schema.objectName && dataSource) { fetchObjectSchema(); } - }, [schema.objectName, dataSource]); + }, [schema.objectName, dataSource, hasInlineFields]); - // Fetch initial data for edit/view modes + // Fetch initial data for edit/view modes (skip if using inline data) useEffect(() => { const fetchInitialData = async () => { if (!schema.recordId || schema.mode === 'create') { - setInitialData(schema.initialValues || {}); + setInitialData(schema.initialData || schema.initialValues || {}); + return; + } + + // Skip fetching if using inline data + if (hasInlineFields) { + return; + } + + if (!dataSource) { + setError(new Error('DataSource is required for fetching record data (inline data not provided)')); + setLoading(false); return; } @@ -100,13 +133,20 @@ export const ObjectForm: React.FC = ({ } }; - if (objectSchema) { + if (objectSchema && !hasInlineFields) { fetchInitialData(); } - }, [schema.objectName, schema.recordId, schema.mode, schema.initialValues, dataSource, objectSchema]); + }, [schema.objectName, schema.recordId, schema.mode, schema.initialValues, schema.initialData, dataSource, objectSchema, hasInlineFields]); - // Generate form fields from object schema + // Generate form fields from object schema or inline fields useEffect(() => { + // For inline fields, use them directly + if (hasInlineFields && schema.customFields) { + setFormFields(schema.customFields); + setLoading(false); + return; + } + if (!objectSchema) return; const generatedFields: FormField[] = []; @@ -207,10 +247,22 @@ export const ObjectForm: React.FC = ({ setFormFields(generatedFields); setLoading(false); - }, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode]); + }, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode, hasInlineFields]); // Handle form submission const handleSubmit = useCallback(async (formData: any) => { + // For inline fields without a dataSource, just call the success callback + if (hasInlineFields && !dataSource) { + if (schema.onSuccess) { + await schema.onSuccess(formData); + } + return formData; + } + + if (!dataSource) { + throw new Error('DataSource is required for form submission (inline mode not configured)'); + } + try { let result; @@ -238,7 +290,7 @@ export const ObjectForm: React.FC = ({ throw err; } - }, [schema, dataSource]); + }, [schema, dataSource, hasInlineFields]); // Handle form cancellation const handleCancel = useCallback(() => { diff --git a/packages/plugin-object/src/ObjectTable.tsx b/packages/plugin-object/src/ObjectTable.tsx index 2f8931494..0dc358403 100644 --- a/packages/plugin-object/src/ObjectTable.tsx +++ b/packages/plugin-object/src/ObjectTable.tsx @@ -26,8 +26,9 @@ export interface ObjectTableProps { /** * ObjectQL data source + * Optional when inline data is provided in schema */ - dataSource: ObjectQLDataSource; + dataSource?: ObjectQLDataSource; /** * Additional CSS class @@ -90,10 +91,24 @@ export const ObjectTable: React.FC = ({ const [columns, setColumns] = useState([]); const [selectedRows, setSelectedRows] = useState([]); - // Fetch object schema from ObjectQL + // Check if using inline data + const hasInlineData = Boolean(schema.data); + + // Initialize with inline data if provided + useEffect(() => { + if (hasInlineData && schema.data) { + setData(schema.data); + setLoading(false); + } + }, [hasInlineData, schema.data]); + + // Fetch object schema from ObjectQL (skip if using inline data) useEffect(() => { const fetchObjectSchema = async () => { try { + if (!dataSource) { + throw new Error('DataSource is required when using ObjectQL schema fetching (inline data not provided)'); + } const schemaData = await dataSource.getObjectSchema(schema.objectName); setObjectSchema(schemaData); } catch (err) { @@ -102,13 +117,43 @@ export const ObjectTable: React.FC = ({ } }; - if (schema.objectName && dataSource) { + // Skip fetching schema if we have inline data and custom columns + if (hasInlineData && schema.columns) { + // Use a minimal schema for inline data with type safety + setObjectSchema({ + name: schema.objectName, + fields: {} as Record, + }); + } else if (schema.objectName && !hasInlineData && dataSource) { fetchObjectSchema(); } - }, [schema.objectName, dataSource]); + }, [schema.objectName, schema.columns, dataSource, hasInlineData]); - // Generate columns from object schema + // Generate columns from object schema or inline data useEffect(() => { + // For inline data with custom columns, use the custom columns directly + if (hasInlineData && schema.columns) { + setColumns(schema.columns); + return; + } + + // For inline data without custom columns, auto-generate from first data row + if (hasInlineData && schema.data && schema.data.length > 0) { + const generatedColumns: TableColumn[] = []; + const firstRow = schema.data[0]; + const fieldsToShow = schema.fields || Object.keys(firstRow); + + fieldsToShow.forEach((fieldName) => { + generatedColumns.push({ + header: fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/_/g, ' '), + accessorKey: fieldName, + }); + }); + + setColumns(generatedColumns); + return; + } + if (!objectSchema) return; const generatedColumns: TableColumn[] = []; @@ -215,11 +260,19 @@ export const ObjectTable: React.FC = ({ } setColumns(generatedColumns); - }, [objectSchema, schema.fields, schema.columns, schema.operations, onEdit, onDelete]); + }, [objectSchema, schema.fields, schema.columns, schema.operations, schema.data, hasInlineData, onEdit, onDelete]); - // Fetch data from ObjectQL + // Fetch data from ObjectQL (skip if using inline data) const fetchData = useCallback(async () => { + // Don't fetch if using inline data + if (hasInlineData) return; + if (!schema.objectName) return; + if (!dataSource) { + setError(new Error('DataSource is required for remote data fetching (inline data not provided)')); + setLoading(false); + return; + } setLoading(true); setError(null); @@ -248,7 +301,7 @@ export const ObjectTable: React.FC = ({ } finally { setLoading(false); } - }, [schema, dataSource]); + }, [schema, dataSource, hasInlineData]); useEffect(() => { if (columns.length > 0) { @@ -287,18 +340,22 @@ export const ObjectTable: React.FC = ({ (item._id || item.id) !== recordId )); - // Call backend delete - await dataSource.delete(schema.objectName, recordId); + // Call backend delete only if we have a dataSource + if (!hasInlineData && dataSource) { + await dataSource.delete(schema.objectName, recordId); + } // Notify parent onDelete(record); } catch (err) { console.error('Failed to delete record:', err); // Revert optimistic update on error - await fetchData(); + if (!hasInlineData) { + await fetchData(); + } alert('Failed to delete record. Please try again.'); } - }, [schema.objectName, dataSource, onDelete, fetchData]); + }, [schema.objectName, dataSource, hasInlineData, onDelete, fetchData]); // Handle bulk delete action const handleBulkDelete = useCallback(async (records: any[]) => { @@ -319,8 +376,10 @@ export const ObjectTable: React.FC = ({ !recordIds.includes(item._id || item.id) )); - // Call backend bulk delete - await dataSource.bulk(schema.objectName, 'delete', records); + // Call backend bulk delete only if we have a dataSource + if (!hasInlineData && dataSource) { + await dataSource.bulk(schema.objectName, 'delete', records); + } // Notify parent onBulkDelete(records); @@ -330,10 +389,12 @@ export const ObjectTable: React.FC = ({ } catch (err) { console.error('Failed to delete records:', err); // Revert optimistic update on error - await fetchData(); + if (!hasInlineData) { + await fetchData(); + } alert('Failed to delete records. Please try again.'); } - }, [schema.objectName, dataSource, onBulkDelete, fetchData]); + }, [schema.objectName, dataSource, hasInlineData, onBulkDelete, fetchData]); // Handle row selection const handleRowSelect = useCallback((rows: any[]) => { diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 488112f74..45e1846c2 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -55,6 +55,13 @@ export interface ObjectTableSchema extends BaseSchema { */ columns?: TableColumn[]; + /** + * Inline data for static/demo tables + * When provided, the table will use this data instead of fetching from a data source. + * Useful for documentation examples and prototyping. + */ + data?: any[]; + /** * Enable/disable built-in operations */ @@ -196,10 +203,18 @@ export interface ObjectFormSchema extends BaseSchema { /** * Custom field configurations - * Overrides auto-generated fields for specific fields + * Overrides auto-generated fields for specific fields. + * When used with inline field definitions (without dataSource), this becomes the primary field source. */ customFields?: FormField[]; + /** + * Inline initial data for demo/static forms + * When provided along with customFields (or inline field definitions), the form can work without a data source. + * Useful for documentation examples and prototyping. + */ + initialData?: Record; + /** * Field groups for organized layout */