Skip to content

Commit 4109f02

Browse files
Copilothotlong
andcommitted
fix: auto-generate columns from object schema in RelatedList when columns missing
- RecordDetailView: pass api (childObject name) in related entries - RelatedList: fetch object schema via dataSource.getObjectSchema() when api/dataSource available but no explicit columns, then generate columns from field metadata (filtering out _-prefixed internal fields) - Add tests for auto-column generation behavior Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 9b0d323 commit 4109f02

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

apps/console/src/components/RecordDetailView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
335335
const related = childRelations.map(({ childObject, childLabel }) => ({
336336
title: childLabel,
337337
type: 'table' as const,
338+
api: childObject,
338339
data: childRelatedData[childObject] || [],
339340
}));
340341

packages/plugin-detail/src/RelatedList.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,21 @@ export const RelatedList: React.FC<RelatedListProps> = ({
7676
const [sortField, setSortField] = React.useState<string | null>(null);
7777
const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc');
7878
const [filterText, setFilterText] = React.useState('');
79+
const [objectSchema, setObjectSchema] = React.useState<any>(null);
7980
const { t } = useDetailTranslation();
8081

8182
// Sync internal state when data prop changes (e.g., parent fetches async data)
8283
React.useEffect(() => {
8384
setRelatedData(data);
8485
}, [data]);
8586

87+
// Auto-fetch object schema when api/dataSource available but columns missing
88+
React.useEffect(() => {
89+
if (api && dataSource?.getObjectSchema && (!columns || columns.length === 0)) {
90+
dataSource.getObjectSchema(api).then(setObjectSchema).catch(() => {});
91+
}
92+
}, [api, dataSource, columns]);
93+
8694
React.useEffect(() => {
8795
if (api && !data.length) {
8896
setLoading(true);
@@ -166,6 +174,18 @@ export const RelatedList: React.FC<RelatedListProps> = ({
166174
}
167175
}, [onRowDelete, t]);
168176

177+
// Generate effective columns from explicit prop or object schema fields
178+
const effectiveColumns = React.useMemo(() => {
179+
if (columns && columns.length > 0) return columns;
180+
if (!objectSchema?.fields) return [];
181+
return Object.entries(objectSchema.fields)
182+
.filter(([key]) => !key.startsWith('_'))
183+
.map(([key, def]: [string, any]) => ({
184+
accessorKey: key,
185+
header: def.label || key,
186+
}));
187+
}, [columns, objectSchema]);
188+
169189
const viewSchema = React.useMemo(() => {
170190
if (schema) return schema;
171191

@@ -176,7 +196,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
176196
return {
177197
type: 'data-table',
178198
data: paginatedData,
179-
columns: columns || [],
199+
columns: effectiveColumns,
180200
pagination: false, // We handle pagination ourselves
181201
pageSize: effectivePageSize || 10,
182202
};
@@ -188,7 +208,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
188208
default:
189209
return { type: 'div', children: 'No view configured' };
190210
}
191-
}, [type, paginatedData, columns, schema, effectivePageSize]);
211+
}, [type, paginatedData, effectiveColumns, schema, effectivePageSize]);
192212

193213
const recordCountText = relatedData.length === 1
194214
? t('detail.relatedRecordOne', { count: relatedData.length })

packages/plugin-detail/src/__tests__/RelatedList.test.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { describe, it, expect, vi } from 'vitest';
10-
import { render, screen, fireEvent } from '@testing-library/react';
10+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
1111
import { RelatedList } from '../RelatedList';
1212

1313
describe('RelatedList', () => {
@@ -63,4 +63,54 @@ describe('RelatedList', () => {
6363
expect(screen.queryByText('New')).not.toBeInTheDocument();
6464
expect(screen.queryByText('View All')).not.toBeInTheDocument();
6565
});
66+
67+
it('should auto-generate columns from object schema when api and dataSource provided but no columns', async () => {
68+
const mockDataSource = {
69+
getObjectSchema: vi.fn().mockResolvedValue({
70+
name: 'order_item',
71+
fields: {
72+
product: { type: 'string', label: 'Product' },
73+
quantity: { type: 'number', label: 'Quantity' },
74+
_id: { type: 'string', label: 'ID' },
75+
},
76+
}),
77+
find: vi.fn(),
78+
} as any;
79+
80+
const data = [{ product: 'Widget', quantity: 5 }];
81+
render(
82+
<RelatedList
83+
title="Order Items"
84+
type="table"
85+
api="order_item"
86+
data={data}
87+
dataSource={mockDataSource}
88+
/>,
89+
);
90+
91+
await waitFor(() => {
92+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('order_item');
93+
});
94+
});
95+
96+
it('should not fetch object schema when explicit columns are provided', () => {
97+
const mockDataSource = {
98+
getObjectSchema: vi.fn(),
99+
find: vi.fn(),
100+
} as any;
101+
102+
const columns = [{ accessorKey: 'name', header: 'Name' }];
103+
render(
104+
<RelatedList
105+
title="Contacts"
106+
type="table"
107+
api="contact"
108+
data={[{ name: 'Alice' }]}
109+
columns={columns}
110+
dataSource={mockDataSource}
111+
/>,
112+
);
113+
114+
expect(mockDataSource.getObjectSchema).not.toHaveBeenCalled();
115+
});
66116
});

0 commit comments

Comments
 (0)