Skip to content

Commit e318006

Browse files
authored
Merge pull request #924 from objectstack-ai/copilot/fix-edit-form-record-issue
2 parents 19d933f + 71f855f commit e318006

File tree

3 files changed

+130
-1
lines changed

3 files changed

+130
-1
lines changed

apps/console/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ export function AppContent() {
400400
description: editingRecord ? `Update details for ${currentObjectDef?.label}` : `Add a new ${currentObjectDef?.label} to your database.`,
401401
open: isDialogOpen,
402402
onOpenChange: setIsDialogOpen,
403+
modalSize: 'lg',
403404
layout: 'vertical',
404405
fields: currentObjectDef.fields
405406
? (Array.isArray(currentObjectDef.fields)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* RecordDetailView — Edit recordId stripping tests
3+
*
4+
* Validates that when editing from the detail page, the objectName prefix
5+
* is stripped from the URL-based recordId before passing to the onEdit callback.
6+
*
7+
* Related: objectstack-ai/objectui — "Record not found" bug
8+
*/
9+
10+
import { describe, it, expect, vi } from 'vitest';
11+
import { render, screen, waitFor } from '@testing-library/react';
12+
import userEvent from '@testing-library/user-event';
13+
import '@testing-library/jest-dom';
14+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
15+
import { RecordDetailView } from '../components/RecordDetailView';
16+
import type { DataSource } from '@object-ui/types';
17+
18+
// ─── Mocks ───────────────────────────────────────────────────────────────────
19+
20+
function createMockDataSource(): DataSource {
21+
return {
22+
async getObjectSchema() {
23+
return {
24+
name: 'contact',
25+
label: 'Contact',
26+
fields: {
27+
name: { name: 'name', label: 'Name', type: 'text' },
28+
email: { name: 'email', label: 'Email', type: 'email' },
29+
},
30+
};
31+
},
32+
findOne: vi.fn().mockResolvedValue({ id: '1772350253615-4', name: 'Alice' }),
33+
find: vi.fn().mockResolvedValue({ data: [] }),
34+
create: vi.fn().mockResolvedValue({ id: '1' }),
35+
update: vi.fn().mockResolvedValue({ id: '1' }),
36+
delete: vi.fn().mockResolvedValue(true),
37+
} as any;
38+
}
39+
40+
const mockObjects = [
41+
{
42+
name: 'contact',
43+
label: 'Contact',
44+
fields: {
45+
name: { name: 'name', label: 'Name', type: 'text' },
46+
email: { name: 'email', label: 'Email', type: 'email' },
47+
},
48+
},
49+
];
50+
51+
function renderDetailView(
52+
urlRecordId: string,
53+
objectName: string,
54+
onEdit: (record: any) => void,
55+
ds?: DataSource,
56+
) {
57+
const dataSource = ds ?? createMockDataSource();
58+
return render(
59+
<MemoryRouter initialEntries={[`/${objectName}/record/${urlRecordId}`]}>
60+
<Routes>
61+
<Route
62+
path="/:objectName/record/:recordId"
63+
element={
64+
<RecordDetailView
65+
dataSource={dataSource}
66+
objects={mockObjects}
67+
onEdit={onEdit}
68+
/>
69+
}
70+
/>
71+
</Routes>
72+
</MemoryRouter>,
73+
);
74+
}
75+
76+
// ─── Tests ───────────────────────────────────────────────────────────────────
77+
78+
describe('RecordDetailView — onEdit recordId stripping', () => {
79+
it('strips objectName prefix from recordId when editing', async () => {
80+
const onEdit = vi.fn();
81+
82+
renderDetailView('contact-1772350253615-4', 'contact', onEdit);
83+
84+
// Wait for the detail view to load
85+
await waitFor(() => {
86+
expect(screen.getByText('Contact')).toBeInTheDocument();
87+
});
88+
89+
// Click the Edit button
90+
const editButton = await screen.findByRole('button', { name: /edit/i });
91+
await userEvent.click(editButton);
92+
93+
// The onEdit callback should receive the STRIPPED recordId (no objectName prefix)
94+
expect(onEdit).toHaveBeenCalledWith(
95+
expect.objectContaining({
96+
id: '1772350253615-4',
97+
_id: '1772350253615-4',
98+
}),
99+
);
100+
});
101+
102+
it('passes recordId as-is when no objectName prefix', async () => {
103+
const onEdit = vi.fn();
104+
105+
renderDetailView('plain-id-12345', 'contact', onEdit);
106+
107+
await waitFor(() => {
108+
expect(screen.getByText('Contact')).toBeInTheDocument();
109+
});
110+
111+
const editButton = await screen.findByRole('button', { name: /edit/i });
112+
await userEvent.click(editButton);
113+
114+
// No prefix to strip — should pass the original recordId unchanged
115+
expect(onEdit).toHaveBeenCalledWith(
116+
expect.objectContaining({
117+
id: 'plain-id-12345',
118+
_id: 'plain-id-12345',
119+
}),
120+
);
121+
});
122+
});

apps/console/src/components/RecordDetailView.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,13 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
277277
<DetailView
278278
schema={detailSchema}
279279
dataSource={dataSource}
280-
onEdit={() => onEdit({ _id: recordId, id: recordId })}
280+
onEdit={() => {
281+
// Strip objectName prefix from URL-based recordId (e.g. "contact-123" → "123")
282+
const pureId = recordId && objectName && recordId.startsWith(`${objectName}-`)
283+
? recordId.slice(objectName.length + 1)
284+
: recordId;
285+
onEdit({ _id: pureId, id: pureId });
286+
}}
281287
/>
282288

283289
{/* Comments & Discussion */}

0 commit comments

Comments
 (0)