Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/plugins/plugin-object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
74 changes: 63 additions & 11 deletions packages/plugin-object/src/ObjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,10 +64,24 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(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) {
Expand All @@ -75,16 +90,34 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
}
};

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<string, any>,
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using an empty object with type assertion bypasses type safety. Consider using a more explicit type or ensuring this minimal schema has proper typing.

Suggested change
fields: {} as Record<string, any>,
fields: {} satisfies Record<string, any>,

Copilot uses AI. Check for mistakes.
});
} 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;
}

Expand All @@ -100,13 +133,20 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
}
};

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[] = [];
Expand Down Expand Up @@ -207,10 +247,22 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({

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;

Expand Down Expand Up @@ -238,7 +290,7 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({

throw err;
}
}, [schema, dataSource]);
}, [schema, dataSource, hasInlineFields]);

// Handle form cancellation
const handleCancel = useCallback(() => {
Expand Down
93 changes: 77 additions & 16 deletions packages/plugin-object/src/ObjectTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -90,10 +91,24 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
const [columns, setColumns] = useState<TableColumn[]>([]);
const [selectedRows, setSelectedRows] = useState<any[]>([]);

// 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) {
Expand All @@ -102,13 +117,43 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
}
};

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<string, any>,
Comment on lines +123 to +125
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using an empty object with type assertion bypasses type safety. Consider using a more explicit type or ensuring this minimal schema has proper typing.

Suggested change
setObjectSchema({
name: schema.objectName,
fields: {} as Record<string, any>,
const inlineFields: Record<string, unknown> = {};
setObjectSchema({
name: schema.objectName,
fields: inlineFields,

Copilot uses AI. Check for mistakes.
});
} 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, ' '),
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header generation logic (capitalize first letter, replace underscores with spaces) is duplicated in the column generation logic elsewhere in the file. Consider extracting this into a helper function to improve maintainability.

Copilot uses AI. Check for mistakes.
accessorKey: fieldName,
});
});

setColumns(generatedColumns);
return;
}

if (!objectSchema) return;

const generatedColumns: TableColumn[] = [];
Expand Down Expand Up @@ -215,11 +260,19 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
}

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);
Expand Down Expand Up @@ -248,7 +301,7 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
} finally {
setLoading(false);
}
}, [schema, dataSource]);
}, [schema, dataSource, hasInlineData]);

useEffect(() => {
if (columns.length > 0) {
Expand Down Expand Up @@ -287,18 +340,22 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
(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[]) => {
Expand All @@ -319,8 +376,10 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
!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);
Expand All @@ -330,10 +389,12 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
} 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[]) => {
Expand Down
17 changes: 16 additions & 1 deletion packages/types/src/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The any[] type is too permissive. Consider defining a more specific type or using a generic type parameter to ensure type safety for the inline data structure.

Suggested change
data?: any[];
data?: Record<string, unknown>[];

Copilot uses AI. Check for mistakes.

/**
* Enable/disable built-in operations
*/
Expand Down Expand Up @@ -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<string, any>;
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Record<string, any> type is too permissive. Consider using a generic type parameter or defining a more specific type to improve type safety for initial data values.

Suggested change
initialData?: Record<string, any>;
initialData?: Record<string, unknown>;

Copilot uses AI. Check for mistakes.

/**
* Field groups for organized layout
*/
Expand Down
Loading