Skip to content

Commit 942aab8

Browse files
authored
Merge pull request #928 from objectstack-ai/copilot/fix-recordid-prefix-issue
2 parents cd7d17b + 298d950 commit 942aab8

File tree

4 files changed

+84
-19
lines changed

4 files changed

+84
-19
lines changed

apps/console/src/__tests__/RecordDetailEdit.test.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,18 @@ function renderDetailView(
7878
describe('RecordDetailView — onEdit recordId stripping', () => {
7979
it('strips objectName prefix from recordId when editing', async () => {
8080
const onEdit = vi.fn();
81+
const ds = createMockDataSource();
8182

82-
renderDetailView('contact-1772350253615-4', 'contact', onEdit);
83+
renderDetailView('contact-1772350253615-4', 'contact', onEdit, ds);
8384

84-
// Wait for the detail view to load
85+
// Wait for the detail view to load (primaryField "name" renders as heading)
8586
await waitFor(() => {
86-
expect(screen.getByText('Contact')).toBeInTheDocument();
87+
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Alice');
8788
});
8889

90+
// findOne should be called with the stripped ID (no objectName prefix)
91+
expect(ds.findOne).toHaveBeenCalledWith('contact', '1772350253615-4');
92+
8993
// Click the Edit button
9094
const editButton = await screen.findByRole('button', { name: /edit/i });
9195
await userEvent.click(editButton);
@@ -101,13 +105,17 @@ describe('RecordDetailView — onEdit recordId stripping', () => {
101105

102106
it('passes recordId as-is when no objectName prefix', async () => {
103107
const onEdit = vi.fn();
108+
const ds = createMockDataSource();
104109

105-
renderDetailView('plain-id-12345', 'contact', onEdit);
110+
renderDetailView('plain-id-12345', 'contact', onEdit, ds);
106111

107112
await waitFor(() => {
108-
expect(screen.getByText('Contact')).toBeInTheDocument();
113+
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Alice');
109114
});
110115

116+
// findOne should be called with the original ID (no prefix to strip)
117+
expect(ds.findOne).toHaveBeenCalledWith('contact', 'plain-id-12345');
118+
111119
const editButton = await screen.findByRole('button', { name: /edit/i });
112120
await userEvent.click(editButton);
113121

apps/console/src/components/RecordDetailView.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,22 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
3434
const [recordViewers, setRecordViewers] = useState<PresenceUser[]>([]);
3535
const objectDef = objects.find((o: any) => o.name === objectName);
3636

37+
// Strip objectName prefix from URL-based recordId (e.g. "contact-123" → "123")
38+
const pureRecordId = recordId && objectName && recordId.startsWith(`${objectName}-`)
39+
? recordId.slice(objectName.length + 1)
40+
: recordId;
41+
3742
const currentUser = user
3843
? { id: user.id, name: user.name, avatar: user.image }
3944
: FALLBACK_USER;
4045

4146
// Fetch presence and comments from API
4247
useEffect(() => {
43-
if (!dataSource || !objectName || !recordId) return;
44-
const threadId = `${objectName}:${recordId}`;
48+
if (!dataSource || !objectName || !pureRecordId) return;
49+
const threadId = `${objectName}:${pureRecordId}`;
4550

4651
// Fetch record viewers
47-
dataSource.find('sys_presence', { $filter: `recordId eq '${recordId}'` })
52+
dataSource.find('sys_presence', { $filter: `recordId eq '${pureRecordId}'` })
4853
.then((res: any) => { if (res.data?.length) setRecordViewers(res.data); })
4954
.catch(() => {});
5055

@@ -72,7 +77,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
7277
}
7378
})
7479
.catch(() => {});
75-
}, [dataSource, objectName, recordId, currentUser]);
80+
}, [dataSource, objectName, pureRecordId, currentUser]);
7681

7782
const handleAddComment = useCallback(
7883
async (text: string) => {
@@ -87,7 +92,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
8792
setFeedItems(prev => [...prev, newItem]);
8893
// Persist to backend
8994
if (dataSource) {
90-
const threadId = `${objectName}:${recordId}`;
95+
const threadId = `${objectName}:${pureRecordId}`;
9196
dataSource.create('sys_comment', {
9297
id: newItem.id,
9398
threadId,
@@ -98,7 +103,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
98103
}).catch(() => {});
99104
}
100105
},
101-
[currentUser, dataSource, objectName, recordId],
106+
[currentUser, dataSource, objectName, pureRecordId],
102107
);
103108

104109
const handleAddReply = useCallback(
@@ -122,7 +127,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
122127
);
123128
});
124129
if (dataSource) {
125-
const threadId = `${objectName}:${recordId}`;
130+
const threadId = `${objectName}:${pureRecordId}`;
126131
dataSource.create('sys_comment', {
127132
id: newItem.id,
128133
threadId,
@@ -134,7 +139,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
134139
}).catch(() => {});
135140
}
136141
},
137-
[currentUser, dataSource, objectName, recordId],
142+
[currentUser, dataSource, objectName, pureRecordId],
138143
);
139144

140145
const handleToggleReaction = useCallback(
@@ -255,7 +260,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
255260
const detailSchema: DetailViewSchema = {
256261
type: 'detail-view',
257262
objectName: objectDef.name,
258-
resourceId: recordId,
263+
resourceId: pureRecordId,
259264
showBack: true,
260265
onBack: 'history',
261266
showEdit: true,
@@ -290,11 +295,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
290295
schema={detailSchema}
291296
dataSource={dataSource}
292297
onEdit={() => {
293-
// Strip objectName prefix from URL-based recordId (e.g. "contact-123" → "123")
294-
const pureId = recordId && objectName && recordId.startsWith(`${objectName}-`)
295-
? recordId.slice(objectName.length + 1)
296-
: recordId;
297-
onEdit({ _id: pureId, id: pureId });
298+
onEdit({ _id: pureRecordId, id: pureRecordId });
298299
}}
299300
/>
300301

packages/plugin-detail/src/DetailView.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,23 @@ export const DetailView: React.FC<DetailViewProps> = ({
232232
);
233233
}
234234

235+
if (!data && !schema.data) {
236+
return (
237+
<div className={cn('flex flex-col items-center justify-center py-16 text-center', className)}>
238+
<p className="text-lg font-semibold">Record not found</p>
239+
<p className="text-sm text-muted-foreground mt-1">
240+
The record you are looking for does not exist or may have been deleted.
241+
</p>
242+
{(schema.showBack ?? true) && (
243+
<Button variant="outline" size="sm" onClick={handleBack} className="mt-4 gap-2">
244+
<ArrowLeft className="h-4 w-4" />
245+
Go back
246+
</Button>
247+
)}
248+
</div>
249+
);
250+
}
251+
235252
return (
236253
<TooltipProvider>
237254
<div className={cn('space-y-6', className)}>

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,4 +499,43 @@ describe('DetailView', () => {
499499
const badgeTexts = Array.from(headerBadges).map(b => b.textContent);
500500
expect(badgeTexts).toContain('Active');
501501
});
502+
503+
it('should show "Record not found" when data is null after loading', async () => {
504+
const mockDataSource = {
505+
findOne: vi.fn().mockResolvedValue(null),
506+
} as any;
507+
508+
const schema: DetailViewSchema = {
509+
type: 'detail-view',
510+
title: 'Contact Details',
511+
objectName: 'contact',
512+
resourceId: 'nonexistent-id',
513+
fields: [{ name: 'name', label: 'Name' }],
514+
};
515+
516+
const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} />);
517+
expect(await findByText('Record not found')).toBeInTheDocument();
518+
expect(await findByText(/does not exist or may have been deleted/)).toBeInTheDocument();
519+
});
520+
521+
it('should show "Go back" button in "Record not found" state when showBack is true', async () => {
522+
const mockDataSource = {
523+
findOne: vi.fn().mockResolvedValue(null),
524+
} as any;
525+
const onBack = vi.fn();
526+
527+
const schema: DetailViewSchema = {
528+
type: 'detail-view',
529+
title: 'Contact Details',
530+
objectName: 'contact',
531+
resourceId: 'nonexistent-id',
532+
fields: [{ name: 'name', label: 'Name' }],
533+
showBack: true,
534+
};
535+
536+
const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} onBack={onBack} />);
537+
const goBackBtn = await findByText('Go back');
538+
fireEvent.click(goBackBtn);
539+
expect(onBack).toHaveBeenCalled();
540+
});
502541
});

0 commit comments

Comments
 (0)