Skip to content

Commit 0e0a97e

Browse files
authored
Merge pull request #149 from objectstack-ai/copilot/fix-loading-users-example
2 parents 31ae310 + efc116d commit 0e0a97e

4 files changed

Lines changed: 163 additions & 34 deletions

File tree

docs/plugins/plugin-object.mdx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ The Object plugin provides seamless integration with ObjectQL backends through s
2323
type: "object-table",
2424
objectName: "users",
2525
columns: [
26-
{ key: "name", label: "Name" },
27-
{ key: "email", label: "Email" },
28-
{ key: "role", label: "Role" },
29-
{ key: "status", label: "Status" }
26+
{ header: "Name", accessorKey: "name" },
27+
{ header: "Email", accessorKey: "email" },
28+
{ header: "Role", accessorKey: "role" },
29+
{ header: "Status", accessorKey: "status" }
3030
],
3131
data: [
3232
{ 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
4545
schema={{
4646
type: "object-form",
4747
objectName: "user",
48-
fields: [
48+
mode: "create",
49+
customFields: [
4950
{
5051
name: "name",
5152
label: "Full Name",
@@ -75,7 +76,7 @@ The Object plugin provides seamless integration with ObjectQL backends through s
7576
defaultValue: "Active"
7677
}
7778
],
78-
submitLabel: "Create User"
79+
submitText: "Create User"
7980
}}
8081
title="ObjectForm Component"
8182
description="Smart form with validation and schema integration"

packages/plugin-object/src/ObjectForm.tsx

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ export interface ObjectFormProps {
2626

2727
/**
2828
* ObjectQL data source
29+
* Optional when using inline field definitions (customFields or fields array with field objects)
2930
*/
30-
dataSource: ObjectQLDataSource;
31+
dataSource?: ObjectQLDataSource;
3132

3233
/**
3334
* Additional CSS class
@@ -63,10 +64,24 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
6364
const [loading, setLoading] = useState(true);
6465
const [error, setError] = useState<Error | null>(null);
6566

66-
// Fetch object schema from ObjectQL
67+
// Check if using inline fields (fields defined as objects, not just names)
68+
const hasInlineFields = schema.customFields && schema.customFields.length > 0;
69+
70+
// Initialize with inline data if provided
71+
useEffect(() => {
72+
if (hasInlineFields) {
73+
setInitialData(schema.initialData || schema.initialValues || {});
74+
setLoading(false);
75+
}
76+
}, [hasInlineFields, schema.initialData, schema.initialValues]);
77+
78+
// Fetch object schema from ObjectQL (skip if using inline fields)
6779
useEffect(() => {
6880
const fetchObjectSchema = async () => {
6981
try {
82+
if (!dataSource) {
83+
throw new Error('DataSource is required when using ObjectQL schema fetching (inline fields not provided)');
84+
}
7085
const schemaData = await dataSource.getObjectSchema(schema.objectName);
7186
setObjectSchema(schemaData);
7287
} catch (err) {
@@ -75,16 +90,34 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
7590
}
7691
};
7792

78-
if (schema.objectName && dataSource) {
93+
// Skip fetching if we have inline fields
94+
if (hasInlineFields) {
95+
// Use a minimal schema for inline fields
96+
setObjectSchema({
97+
name: schema.objectName,
98+
fields: {} as Record<string, any>,
99+
});
100+
} else if (schema.objectName && dataSource) {
79101
fetchObjectSchema();
80102
}
81-
}, [schema.objectName, dataSource]);
103+
}, [schema.objectName, dataSource, hasInlineFields]);
82104

83-
// Fetch initial data for edit/view modes
105+
// Fetch initial data for edit/view modes (skip if using inline data)
84106
useEffect(() => {
85107
const fetchInitialData = async () => {
86108
if (!schema.recordId || schema.mode === 'create') {
87-
setInitialData(schema.initialValues || {});
109+
setInitialData(schema.initialData || schema.initialValues || {});
110+
return;
111+
}
112+
113+
// Skip fetching if using inline data
114+
if (hasInlineFields) {
115+
return;
116+
}
117+
118+
if (!dataSource) {
119+
setError(new Error('DataSource is required for fetching record data (inline data not provided)'));
120+
setLoading(false);
88121
return;
89122
}
90123

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

103-
if (objectSchema) {
136+
if (objectSchema && !hasInlineFields) {
104137
fetchInitialData();
105138
}
106-
}, [schema.objectName, schema.recordId, schema.mode, schema.initialValues, dataSource, objectSchema]);
139+
}, [schema.objectName, schema.recordId, schema.mode, schema.initialValues, schema.initialData, dataSource, objectSchema, hasInlineFields]);
107140

108-
// Generate form fields from object schema
141+
// Generate form fields from object schema or inline fields
109142
useEffect(() => {
143+
// For inline fields, use them directly
144+
if (hasInlineFields && schema.customFields) {
145+
setFormFields(schema.customFields);
146+
setLoading(false);
147+
return;
148+
}
149+
110150
if (!objectSchema) return;
111151

112152
const generatedFields: FormField[] = [];
@@ -207,10 +247,22 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
207247

208248
setFormFields(generatedFields);
209249
setLoading(false);
210-
}, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode]);
250+
}, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode, hasInlineFields]);
211251

212252
// Handle form submission
213253
const handleSubmit = useCallback(async (formData: any) => {
254+
// For inline fields without a dataSource, just call the success callback
255+
if (hasInlineFields && !dataSource) {
256+
if (schema.onSuccess) {
257+
await schema.onSuccess(formData);
258+
}
259+
return formData;
260+
}
261+
262+
if (!dataSource) {
263+
throw new Error('DataSource is required for form submission (inline mode not configured)');
264+
}
265+
214266
try {
215267
let result;
216268

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

239291
throw err;
240292
}
241-
}, [schema, dataSource]);
293+
}, [schema, dataSource, hasInlineFields]);
242294

243295
// Handle form cancellation
244296
const handleCancel = useCallback(() => {

packages/plugin-object/src/ObjectTable.tsx

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ export interface ObjectTableProps {
2626

2727
/**
2828
* ObjectQL data source
29+
* Optional when inline data is provided in schema
2930
*/
30-
dataSource: ObjectQLDataSource;
31+
dataSource?: ObjectQLDataSource;
3132

3233
/**
3334
* Additional CSS class
@@ -90,10 +91,24 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
9091
const [columns, setColumns] = useState<TableColumn[]>([]);
9192
const [selectedRows, setSelectedRows] = useState<any[]>([]);
9293

93-
// Fetch object schema from ObjectQL
94+
// Check if using inline data
95+
const hasInlineData = Boolean(schema.data);
96+
97+
// Initialize with inline data if provided
98+
useEffect(() => {
99+
if (hasInlineData && schema.data) {
100+
setData(schema.data);
101+
setLoading(false);
102+
}
103+
}, [hasInlineData, schema.data]);
104+
105+
// Fetch object schema from ObjectQL (skip if using inline data)
94106
useEffect(() => {
95107
const fetchObjectSchema = async () => {
96108
try {
109+
if (!dataSource) {
110+
throw new Error('DataSource is required when using ObjectQL schema fetching (inline data not provided)');
111+
}
97112
const schemaData = await dataSource.getObjectSchema(schema.objectName);
98113
setObjectSchema(schemaData);
99114
} catch (err) {
@@ -102,13 +117,43 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
102117
}
103118
};
104119

105-
if (schema.objectName && dataSource) {
120+
// Skip fetching schema if we have inline data and custom columns
121+
if (hasInlineData && schema.columns) {
122+
// Use a minimal schema for inline data with type safety
123+
setObjectSchema({
124+
name: schema.objectName,
125+
fields: {} as Record<string, any>,
126+
});
127+
} else if (schema.objectName && !hasInlineData && dataSource) {
106128
fetchObjectSchema();
107129
}
108-
}, [schema.objectName, dataSource]);
130+
}, [schema.objectName, schema.columns, dataSource, hasInlineData]);
109131

110-
// Generate columns from object schema
132+
// Generate columns from object schema or inline data
111133
useEffect(() => {
134+
// For inline data with custom columns, use the custom columns directly
135+
if (hasInlineData && schema.columns) {
136+
setColumns(schema.columns);
137+
return;
138+
}
139+
140+
// For inline data without custom columns, auto-generate from first data row
141+
if (hasInlineData && schema.data && schema.data.length > 0) {
142+
const generatedColumns: TableColumn[] = [];
143+
const firstRow = schema.data[0];
144+
const fieldsToShow = schema.fields || Object.keys(firstRow);
145+
146+
fieldsToShow.forEach((fieldName) => {
147+
generatedColumns.push({
148+
header: fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/_/g, ' '),
149+
accessorKey: fieldName,
150+
});
151+
});
152+
153+
setColumns(generatedColumns);
154+
return;
155+
}
156+
112157
if (!objectSchema) return;
113158

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

217262
setColumns(generatedColumns);
218-
}, [objectSchema, schema.fields, schema.columns, schema.operations, onEdit, onDelete]);
263+
}, [objectSchema, schema.fields, schema.columns, schema.operations, schema.data, hasInlineData, onEdit, onDelete]);
219264

220-
// Fetch data from ObjectQL
265+
// Fetch data from ObjectQL (skip if using inline data)
221266
const fetchData = useCallback(async () => {
267+
// Don't fetch if using inline data
268+
if (hasInlineData) return;
269+
222270
if (!schema.objectName) return;
271+
if (!dataSource) {
272+
setError(new Error('DataSource is required for remote data fetching (inline data not provided)'));
273+
setLoading(false);
274+
return;
275+
}
223276

224277
setLoading(true);
225278
setError(null);
@@ -248,7 +301,7 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
248301
} finally {
249302
setLoading(false);
250303
}
251-
}, [schema, dataSource]);
304+
}, [schema, dataSource, hasInlineData]);
252305

253306
useEffect(() => {
254307
if (columns.length > 0) {
@@ -287,18 +340,22 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
287340
(item._id || item.id) !== recordId
288341
));
289342

290-
// Call backend delete
291-
await dataSource.delete(schema.objectName, recordId);
343+
// Call backend delete only if we have a dataSource
344+
if (!hasInlineData && dataSource) {
345+
await dataSource.delete(schema.objectName, recordId);
346+
}
292347

293348
// Notify parent
294349
onDelete(record);
295350
} catch (err) {
296351
console.error('Failed to delete record:', err);
297352
// Revert optimistic update on error
298-
await fetchData();
353+
if (!hasInlineData) {
354+
await fetchData();
355+
}
299356
alert('Failed to delete record. Please try again.');
300357
}
301-
}, [schema.objectName, dataSource, onDelete, fetchData]);
358+
}, [schema.objectName, dataSource, hasInlineData, onDelete, fetchData]);
302359

303360
// Handle bulk delete action
304361
const handleBulkDelete = useCallback(async (records: any[]) => {
@@ -319,8 +376,10 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
319376
!recordIds.includes(item._id || item.id)
320377
));
321378

322-
// Call backend bulk delete
323-
await dataSource.bulk(schema.objectName, 'delete', records);
379+
// Call backend bulk delete only if we have a dataSource
380+
if (!hasInlineData && dataSource) {
381+
await dataSource.bulk(schema.objectName, 'delete', records);
382+
}
324383

325384
// Notify parent
326385
onBulkDelete(records);
@@ -330,10 +389,12 @@ export const ObjectTable: React.FC<ObjectTableProps> = ({
330389
} catch (err) {
331390
console.error('Failed to delete records:', err);
332391
// Revert optimistic update on error
333-
await fetchData();
392+
if (!hasInlineData) {
393+
await fetchData();
394+
}
334395
alert('Failed to delete records. Please try again.');
335396
}
336-
}, [schema.objectName, dataSource, onBulkDelete, fetchData]);
397+
}, [schema.objectName, dataSource, hasInlineData, onBulkDelete, fetchData]);
337398

338399
// Handle row selection
339400
const handleRowSelect = useCallback((rows: any[]) => {

packages/types/src/objectql.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ export interface ObjectTableSchema extends BaseSchema {
5555
*/
5656
columns?: TableColumn[];
5757

58+
/**
59+
* Inline data for static/demo tables
60+
* When provided, the table will use this data instead of fetching from a data source.
61+
* Useful for documentation examples and prototyping.
62+
*/
63+
data?: any[];
64+
5865
/**
5966
* Enable/disable built-in operations
6067
*/
@@ -196,10 +203,18 @@ export interface ObjectFormSchema extends BaseSchema {
196203

197204
/**
198205
* Custom field configurations
199-
* Overrides auto-generated fields for specific fields
206+
* Overrides auto-generated fields for specific fields.
207+
* When used with inline field definitions (without dataSource), this becomes the primary field source.
200208
*/
201209
customFields?: FormField[];
202210

211+
/**
212+
* Inline initial data for demo/static forms
213+
* When provided along with customFields (or inline field definitions), the form can work without a data source.
214+
* Useful for documentation examples and prototyping.
215+
*/
216+
initialData?: Record<string, any>;
217+
203218
/**
204219
* Field groups for organized layout
205220
*/

0 commit comments

Comments
 (0)