From d48b28c45cbc39a238896ad352fdc97326bd0198 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 09:13:14 +0000 Subject: [PATCH 1/3] Initial plan From 940cfa61adb45df49dea8d148c76ffa5c59948f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 09:18:54 +0000 Subject: [PATCH 2/3] fix: strip objectName prefix from recordId in RecordDetailView and show "Record not found" in DetailView - RecordDetailView: compute pureRecordId once and use for resourceId, presence, and comment queries - DetailView: show "Record not found" message with Go back button when data is null after loading - Update tests to verify findOne called with stripped ID and test Record not found state Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/__tests__/RecordDetailEdit.test.tsx | 18 +++++--- .../src/components/RecordDetailView.tsx | 29 ++++++------- packages/plugin-detail/src/DetailView.tsx | 17 ++++++++ .../src/__tests__/DetailView.test.tsx | 41 +++++++++++++++++++ 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/apps/console/src/__tests__/RecordDetailEdit.test.tsx b/apps/console/src/__tests__/RecordDetailEdit.test.tsx index ff655720ca..4c4a0f8a96 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 bd1b592ad7..b7adc5a041 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 a6b339f37e..ea5336ab3f 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 3868dccd8f..2ddc252452 100644 --- a/packages/plugin-detail/src/__tests__/DetailView.test.tsx +++ b/packages/plugin-detail/src/__tests__/DetailView.test.tsx @@ -499,4 +499,45 @@ 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(); + }); }); From 298d95065882c37066a9778acc843dd181579e2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 09:21:32 +0000 Subject: [PATCH 3/3] style: remove extra blank lines in DetailView test for consistency Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/plugin-detail/src/__tests__/DetailView.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/plugin-detail/src/__tests__/DetailView.test.tsx b/packages/plugin-detail/src/__tests__/DetailView.test.tsx index 2ddc252452..caa77314cc 100644 --- a/packages/plugin-detail/src/__tests__/DetailView.test.tsx +++ b/packages/plugin-detail/src/__tests__/DetailView.test.tsx @@ -514,7 +514,6 @@ describe('DetailView', () => { }; const { findByText } = render(); - expect(await findByText('Record not found')).toBeInTheDocument(); expect(await findByText(/does not exist or may have been deleted/)).toBeInTheDocument(); }); @@ -535,7 +534,6 @@ describe('DetailView', () => { }; const { findByText } = render(); - const goBackBtn = await findByText('Go back'); fireEvent.click(goBackBtn); expect(onBack).toHaveBeenCalled();