diff --git a/apps/console/src/__tests__/RecordDetailEdit.test.tsx b/apps/console/src/__tests__/RecordDetailEdit.test.tsx index ff655720c..4c4a0f8a9 100644 --- a/apps/console/src/__tests__/RecordDetailEdit.test.tsx +++ b/apps/console/src/__tests__/RecordDetailEdit.test.tsx @@ -78,14 +78,18 @@ function renderDetailView( describe('RecordDetailView — onEdit recordId stripping', () => { it('strips objectName prefix from recordId when editing', async () => { const onEdit = vi.fn(); + const ds = createMockDataSource(); - renderDetailView('contact-1772350253615-4', 'contact', onEdit); + renderDetailView('contact-1772350253615-4', 'contact', onEdit, ds); - // Wait for the detail view to load + // Wait for the detail view to load (primaryField "name" renders as heading) await waitFor(() => { - expect(screen.getByText('Contact')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Alice'); }); + // findOne should be called with the stripped ID (no objectName prefix) + expect(ds.findOne).toHaveBeenCalledWith('contact', '1772350253615-4'); + // Click the Edit button const editButton = await screen.findByRole('button', { name: /edit/i }); await userEvent.click(editButton); @@ -101,13 +105,17 @@ describe('RecordDetailView — onEdit recordId stripping', () => { it('passes recordId as-is when no objectName prefix', async () => { const onEdit = vi.fn(); + const ds = createMockDataSource(); - renderDetailView('plain-id-12345', 'contact', onEdit); + renderDetailView('plain-id-12345', 'contact', onEdit, ds); await waitFor(() => { - expect(screen.getByText('Contact')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Alice'); }); + // findOne should be called with the original ID (no prefix to strip) + expect(ds.findOne).toHaveBeenCalledWith('contact', 'plain-id-12345'); + const editButton = await screen.findByRole('button', { name: /edit/i }); await userEvent.click(editButton); diff --git a/apps/console/src/components/RecordDetailView.tsx b/apps/console/src/components/RecordDetailView.tsx index bd1b592ad..b7adc5a04 100644 --- a/apps/console/src/components/RecordDetailView.tsx +++ b/apps/console/src/components/RecordDetailView.tsx @@ -34,17 +34,22 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi const [recordViewers, setRecordViewers] = useState([]); const objectDef = objects.find((o: any) => o.name === objectName); + // Strip objectName prefix from URL-based recordId (e.g. "contact-123" → "123") + const pureRecordId = recordId && objectName && recordId.startsWith(`${objectName}-`) + ? recordId.slice(objectName.length + 1) + : recordId; + const currentUser = user ? { id: user.id, name: user.name, avatar: user.image } : FALLBACK_USER; // Fetch presence and comments from API useEffect(() => { - if (!dataSource || !objectName || !recordId) return; - const threadId = `${objectName}:${recordId}`; + if (!dataSource || !objectName || !pureRecordId) return; + const threadId = `${objectName}:${pureRecordId}`; // Fetch record viewers - dataSource.find('sys_presence', { $filter: `recordId eq '${recordId}'` }) + dataSource.find('sys_presence', { $filter: `recordId eq '${pureRecordId}'` }) .then((res: any) => { if (res.data?.length) setRecordViewers(res.data); }) .catch(() => {}); @@ -72,7 +77,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi } }) .catch(() => {}); - }, [dataSource, objectName, recordId, currentUser]); + }, [dataSource, objectName, pureRecordId, currentUser]); const handleAddComment = useCallback( async (text: string) => { @@ -87,7 +92,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi setFeedItems(prev => [...prev, newItem]); // Persist to backend if (dataSource) { - const threadId = `${objectName}:${recordId}`; + const threadId = `${objectName}:${pureRecordId}`; dataSource.create('sys_comment', { id: newItem.id, threadId, @@ -98,7 +103,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi }).catch(() => {}); } }, - [currentUser, dataSource, objectName, recordId], + [currentUser, dataSource, objectName, pureRecordId], ); const handleAddReply = useCallback( @@ -122,7 +127,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi ); }); if (dataSource) { - const threadId = `${objectName}:${recordId}`; + const threadId = `${objectName}:${pureRecordId}`; dataSource.create('sys_comment', { id: newItem.id, threadId, @@ -134,7 +139,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi }).catch(() => {}); } }, - [currentUser, dataSource, objectName, recordId], + [currentUser, dataSource, objectName, pureRecordId], ); const handleToggleReaction = useCallback( @@ -250,7 +255,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi const detailSchema: DetailViewSchema = { type: 'detail-view', objectName: objectDef.name, - resourceId: recordId, + resourceId: pureRecordId, showBack: true, onBack: 'history', showEdit: true, @@ -278,11 +283,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi schema={detailSchema} dataSource={dataSource} onEdit={() => { - // Strip objectName prefix from URL-based recordId (e.g. "contact-123" → "123") - const pureId = recordId && objectName && recordId.startsWith(`${objectName}-`) - ? recordId.slice(objectName.length + 1) - : recordId; - onEdit({ _id: pureId, id: pureId }); + onEdit({ _id: pureRecordId, id: pureRecordId }); }} /> diff --git a/packages/plugin-detail/src/DetailView.tsx b/packages/plugin-detail/src/DetailView.tsx index a6b339f37..ea5336ab3 100644 --- a/packages/plugin-detail/src/DetailView.tsx +++ b/packages/plugin-detail/src/DetailView.tsx @@ -232,6 +232,23 @@ export const DetailView: React.FC = ({ ); } + if (!data && !schema.data) { + return ( +
+

Record not found

+

+ The record you are looking for does not exist or may have been deleted. +

+ {(schema.showBack ?? true) && ( + + )} +
+ ); + } + return (
diff --git a/packages/plugin-detail/src/__tests__/DetailView.test.tsx b/packages/plugin-detail/src/__tests__/DetailView.test.tsx index 3868dccd8..caa77314c 100644 --- a/packages/plugin-detail/src/__tests__/DetailView.test.tsx +++ b/packages/plugin-detail/src/__tests__/DetailView.test.tsx @@ -499,4 +499,43 @@ describe('DetailView', () => { const badgeTexts = Array.from(headerBadges).map(b => b.textContent); expect(badgeTexts).toContain('Active'); }); + + it('should show "Record not found" when data is null after loading', async () => { + const mockDataSource = { + findOne: vi.fn().mockResolvedValue(null), + } as any; + + const schema: DetailViewSchema = { + type: 'detail-view', + title: 'Contact Details', + objectName: 'contact', + resourceId: 'nonexistent-id', + fields: [{ name: 'name', label: 'Name' }], + }; + + const { findByText } = render(); + expect(await findByText('Record not found')).toBeInTheDocument(); + expect(await findByText(/does not exist or may have been deleted/)).toBeInTheDocument(); + }); + + it('should show "Go back" button in "Record not found" state when showBack is true', async () => { + const mockDataSource = { + findOne: vi.fn().mockResolvedValue(null), + } as any; + const onBack = vi.fn(); + + const schema: DetailViewSchema = { + type: 'detail-view', + title: 'Contact Details', + objectName: 'contact', + resourceId: 'nonexistent-id', + fields: [{ name: 'name', label: 'Name' }], + showBack: true, + }; + + const { findByText } = render(); + const goBackBtn = await findByText('Go back'); + fireEvent.click(goBackBtn); + expect(onBack).toHaveBeenCalled(); + }); });