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
1 change: 1 addition & 0 deletions apps/console/src/components/RecordDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
const related = childRelations.map(({ childObject, childLabel }) => ({
title: childLabel,
type: 'table' as const,
api: childObject,
data: childRelatedData[childObject] || [],
}));

Expand Down
26 changes: 24 additions & 2 deletions packages/plugin-detail/src/RelatedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,23 @@ export const RelatedList: React.FC<RelatedListProps> = ({
const [sortField, setSortField] = React.useState<string | null>(null);
const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc');
const [filterText, setFilterText] = React.useState('');
const [objectSchema, setObjectSchema] = React.useState<any>(null);
const { t } = useDetailTranslation();

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

// Auto-fetch object schema when api/dataSource available but columns missing
React.useEffect(() => {
if (api && dataSource?.getObjectSchema && !columns?.length) {
dataSource.getObjectSchema(api).then(setObjectSchema).catch((err: unknown) => {
console.warn(`[RelatedList] Failed to fetch schema for ${api}:`, err);
});
}
}, [api, dataSource, columns]);
Comment on lines +88 to +94
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

When the api prop changes (e.g., when the component is reused for a different child object), the objectSchema state is not reset to null before the new getObjectSchema() call resolves. This means there's a brief window where the stale schema from the previous api is used to generate effectiveColumns. Adding a setObjectSchema(null) call at the start of the effect (before the async fetch) would prevent briefly showing wrong columns from the old schema.

Copilot uses AI. Check for mistakes.

React.useEffect(() => {
if (api && !data.length) {
setLoading(true);
Expand Down Expand Up @@ -166,6 +176,18 @@ export const RelatedList: React.FC<RelatedListProps> = ({
}
}, [onRowDelete, t]);

// Generate effective columns from explicit prop or object schema fields
const effectiveColumns = React.useMemo(() => {
if (columns && columns.length > 0) return columns;
if (!objectSchema?.fields) return [];
return Object.entries(objectSchema.fields)
.filter(([key]) => !key.startsWith('_'))
.map(([key, def]: [string, any]) => ({
accessorKey: key,
header: def.label || key,
}));
}, [columns, objectSchema]);

const viewSchema = React.useMemo(() => {
if (schema) return schema;

Expand All @@ -176,7 +198,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
return {
type: 'data-table',
data: paginatedData,
columns: columns || [],
columns: effectiveColumns,
Comment on lines 180 to +201
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The new effectiveColumns variable (derived from schema when no explicit columns prop is set) is correctly used in viewSchema (line 201), but the sortable column headers rendering block elsewhere in the component still checks columns && columns.length > 0 and iterates over columns (the raw prop), not effectiveColumns. As a result, when sortable={true} is used without an explicit columns prop, the sort buttons will never render even when effectiveColumns has schema-derived values. That block should use effectiveColumns instead of columns.

Copilot uses AI. Check for mistakes.
pagination: false, // We handle pagination ourselves
pageSize: effectivePageSize || 10,
};
Expand All @@ -188,7 +210,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
default:
return { type: 'div', children: 'No view configured' };
}
}, [type, paginatedData, columns, schema, effectivePageSize]);
}, [type, paginatedData, effectiveColumns, schema, effectivePageSize]);

const recordCountText = relatedData.length === 1
? t('detail.relatedRecordOne', { count: relatedData.length })
Expand Down
60 changes: 59 additions & 1 deletion packages/plugin-detail/src/__tests__/RelatedList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

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

describe('RelatedList', () => {
Expand Down Expand Up @@ -63,4 +63,62 @@ describe('RelatedList', () => {
expect(screen.queryByText('New')).not.toBeInTheDocument();
expect(screen.queryByText('View All')).not.toBeInTheDocument();
});

it('should auto-generate columns from object schema when api and dataSource provided but no columns', async () => {
const mockDataSource = {
getObjectSchema: vi.fn().mockResolvedValue({
name: 'order_item',
fields: {
product: { type: 'string', label: 'Product' },
quantity: { type: 'number', label: 'Quantity' },
_id: { type: 'string', label: 'ID' },
},
}),
find: vi.fn(),
} as any;

const data = [{ product: 'Widget', quantity: 5 }];
render(
<RelatedList
title="Order Items"
type="table"
api="order_item"
data={data}
dataSource={mockDataSource}
/>,
);

await waitFor(() => {
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('order_item');
});

// Verify columns are generated from schema (excluding _id)
await waitFor(() => {
expect(screen.getByText('Product')).toBeInTheDocument();
expect(screen.getByText('Quantity')).toBeInTheDocument();
});
// _id should be filtered out
expect(screen.queryByText('ID')).not.toBeInTheDocument();
});

it('should not fetch object schema when explicit columns are provided', () => {
const mockDataSource = {
getObjectSchema: vi.fn(),
find: vi.fn(),
} as any;

const columns = [{ accessorKey: 'name', header: 'Name' }];
render(
<RelatedList
title="Contacts"
type="table"
api="contact"
data={[{ name: 'Alice' }]}
columns={columns}
dataSource={mockDataSource}
/>,
);

expect(mockDataSource.getObjectSchema).not.toHaveBeenCalled();
});
});