Skip to content

Commit 2b1cca3

Browse files
Copilothotlong
andcommitted
fix: stop stripping objectName prefix from URL recordId in RecordDetailView
The navigation code passes record._id directly into the URL without adding any prefix. Stripping the prefix incorrectly truncated backend-generated IDs (e.g. "opportunity-1772370455623-1" → "1772370455623-1"), causing findOne to fail with "Record not found". Also adds robust fallback in DetailView: if findOne returns null, retry with the prefix stripped or prepended for maximum backward compatibility. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a87ad07 commit 2b1cca3

File tree

3 files changed

+47
-20
lines changed

3 files changed

+47
-20
lines changed

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
2-
* RecordDetailView — Edit recordId stripping tests
2+
* RecordDetailView — recordId handling tests
33
*
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.
4+
* Validates that the URL-based recordId is passed through as-is to the
5+
* findOne API and onEdit callback. The navigation code puts the actual
6+
* record._id into the URL, so no prefix stripping is needed.
67
*
78
* Related: objectstack-ai/objectui — "Record not found" bug
89
*/
@@ -29,7 +30,7 @@ function createMockDataSource(): DataSource {
2930
},
3031
};
3132
},
32-
findOne: vi.fn().mockResolvedValue({ id: '1772350253615-4', name: 'Alice' }),
33+
findOne: vi.fn().mockResolvedValue({ id: 'contact-1772350253615-4', name: 'Alice' }),
3334
find: vi.fn().mockResolvedValue({ data: [] }),
3435
create: vi.fn().mockResolvedValue({ id: '1' }),
3536
update: vi.fn().mockResolvedValue({ id: '1' }),
@@ -75,8 +76,8 @@ function renderDetailView(
7576

7677
// ─── Tests ───────────────────────────────────────────────────────────────────
7778

78-
describe('RecordDetailView — onEdit recordId stripping', () => {
79-
it('strips objectName prefix from recordId when editing', async () => {
79+
describe('RecordDetailView — recordId handling', () => {
80+
it('passes URL recordId as-is to findOne (with objectName prefix)', async () => {
8081
const onEdit = vi.fn();
8182
const ds = createMockDataSource();
8283

@@ -87,18 +88,18 @@ describe('RecordDetailView — onEdit recordId stripping', () => {
8788
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Alice');
8889
});
8990

90-
// findOne should be called with the stripped ID (no objectName prefix)
91-
expect(ds.findOne).toHaveBeenCalledWith('contact', '1772350253615-4');
91+
// findOne should be called with the FULL URL recordId (no stripping)
92+
expect(ds.findOne).toHaveBeenCalledWith('contact', 'contact-1772350253615-4');
9293

9394
// Click the Edit button
9495
const editButton = await screen.findByRole('button', { name: /edit/i });
9596
await userEvent.click(editButton);
9697

97-
// The onEdit callback should receive the STRIPPED recordId (no objectName prefix)
98+
// The onEdit callback should receive the FULL recordId
9899
expect(onEdit).toHaveBeenCalledWith(
99100
expect.objectContaining({
100-
id: '1772350253615-4',
101-
_id: '1772350253615-4',
101+
id: 'contact-1772350253615-4',
102+
_id: 'contact-1772350253615-4',
102103
}),
103104
);
104105
});
@@ -113,13 +114,13 @@ describe('RecordDetailView — onEdit recordId stripping', () => {
113114
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Alice');
114115
});
115116

116-
// findOne should be called with the original ID (no prefix to strip)
117+
// findOne should be called with the original ID unchanged
117118
expect(ds.findOne).toHaveBeenCalledWith('contact', 'plain-id-12345');
118119

119120
const editButton = await screen.findByRole('button', { name: /edit/i });
120121
await userEvent.click(editButton);
121122

122-
// No prefix to strip — should pass the original recordId unchanged
123+
// Should pass the original recordId unchanged
123124
expect(onEdit).toHaveBeenCalledWith(
124125
expect.objectContaining({
125126
id: 'plain-id-12345',

apps/console/src/components/RecordDetailView.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ 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;
37+
// Use the URL recordId as-is — it contains the actual record _id.
38+
// Navigation code passes `record._id || record.id` directly into the URL
39+
// without adding any prefix, so no stripping is needed.
40+
const pureRecordId = recordId;
4141

4242
const currentUser = user
4343
? { id: user.id, name: user.name, avatar: user.image }

packages/plugin-detail/src/DetailView.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,35 @@ export const DetailView: React.FC<DetailViewProps> = ({
8585

8686
if (dataSource && schema.objectName && schema.resourceId) {
8787
setLoading(true);
88-
dataSource.findOne(schema.objectName, schema.resourceId).then((result) => {
89-
setData(result);
90-
setLoading(false);
88+
const objectName = schema.objectName;
89+
const resourceId = schema.resourceId;
90+
const prefix = `${objectName}-`;
91+
92+
dataSource.findOne(objectName, resourceId).then((result) => {
93+
if (result) {
94+
setData(result);
95+
setLoading(false);
96+
return;
97+
}
98+
// Fallback: if not found and resourceId starts with objectName prefix,
99+
// retry with the prefix stripped (handles legacy ID conventions)
100+
if (resourceId.startsWith(prefix)) {
101+
const strippedId = resourceId.slice(prefix.length);
102+
return dataSource.findOne(objectName, strippedId).then((fallbackResult) => {
103+
setData(fallbackResult);
104+
setLoading(false);
105+
});
106+
}
107+
// Fallback: if not found and resourceId does NOT have the prefix,
108+
// retry with the prefix prepended (handles bare IDs)
109+
if (!resourceId.startsWith(prefix)) {
110+
return dataSource.findOne(objectName, `${prefix}${resourceId}`).then((fallbackResult) => {
111+
setData(fallbackResult);
112+
setLoading(false);
113+
});
114+
}
115+
setData(null);
116+
setLoading(false);
91117
}).catch((err) => {
92118
console.error('Failed to fetch detail data:', err);
93119
setLoading(false);

0 commit comments

Comments
 (0)