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
18 changes: 13 additions & 5 deletions apps/console/src/__tests__/RecordDetailEdit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down
29 changes: 15 additions & 14 deletions apps/console/src/components/RecordDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
const [recordViewers, setRecordViewers] = useState<PresenceUser[]>([]);
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(() => {});

Expand Down Expand Up @@ -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) => {
Expand All @@ -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,
Expand All @@ -98,7 +103,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
}).catch(() => {});
}
},
[currentUser, dataSource, objectName, recordId],
[currentUser, dataSource, objectName, pureRecordId],
);

const handleAddReply = useCallback(
Expand All @@ -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,
Expand All @@ -134,7 +139,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
}).catch(() => {});
}
},
[currentUser, dataSource, objectName, recordId],
[currentUser, dataSource, objectName, pureRecordId],
);

const handleToggleReaction = useCallback(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
}}
/>

Expand Down
17 changes: 17 additions & 0 deletions packages/plugin-detail/src/DetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,23 @@ export const DetailView: React.FC<DetailViewProps> = ({
);
}

if (!data && !schema.data) {
return (
<div className={cn('flex flex-col items-center justify-center py-16 text-center', className)}>
<p className="text-lg font-semibold">Record not found</p>
<p className="text-sm text-muted-foreground mt-1">
The record you are looking for does not exist or may have been deleted.
</p>
{(schema.showBack ?? true) && (
<Button variant="outline" size="sm" onClick={handleBack} className="mt-4 gap-2">
<ArrowLeft className="h-4 w-4" />
Go back
</Button>
)}
</div>
);
}

return (
<TooltipProvider>
<div className={cn('space-y-6', className)}>
Expand Down
39 changes: 39 additions & 0 deletions packages/plugin-detail/src/__tests__/DetailView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<DetailView schema={schema} dataSource={mockDataSource} />);
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(<DetailView schema={schema} dataSource={mockDataSource} onBack={onBack} />);
const goBackBtn = await findByText('Go back');
fireEvent.click(goBackBtn);
expect(onBack).toHaveBeenCalled();
});
});