Skip to content

Commit 8744a85

Browse files
Copilothotlong
andcommitted
feat: add object detail page with URL-based navigation and field management entry
- Add /system/objects/:objectName route for object detail pages - ObjectManagerPage now uses URL-based routing (not state toggle) - Object detail view shows: properties card (API name, label, group, status, field count, relationships) + FieldDesigner for field management - Clicking object name in ObjectGrid navigates to detail URL - Back button returns to object list - Tests: 11 tests covering list view, detail view, URL navigation, properties, field management section, relationships, back navigation Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/d101c93c-eeee-40ac-9495-b85336c1f08f Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent e84c42e commit 8744a85

3 files changed

Lines changed: 226 additions & 95 deletions

File tree

apps/console/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ export function AppContent() {
289289
<Route path="system" element={<SystemHubPage />} />
290290
<Route path="system/apps" element={<AppManagementPage />} />
291291
<Route path="system/objects" element={<ObjectManagerPage />} />
292+
<Route path="system/objects/:objectName" element={<ObjectManagerPage />} />
292293
<Route path="system/users" element={<UserManagementPage />} />
293294
<Route path="system/organizations" element={<OrgManagementPage />} />
294295
<Route path="system/roles" element={<RoleManagementPage />} />
@@ -382,6 +383,7 @@ export function AppContent() {
382383
<Route path="system" element={<SystemHubPage />} />
383384
<Route path="system/apps" element={<AppManagementPage />} />
384385
<Route path="system/objects" element={<ObjectManagerPage />} />
386+
<Route path="system/objects/:objectName" element={<ObjectManagerPage />} />
385387
<Route path="system/users" element={<UserManagementPage />} />
386388
<Route path="system/organizations" element={<OrgManagementPage />} />
387389
<Route path="system/roles" element={<RoleManagementPage />} />

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

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*
44
* Tests for the system administration Object Manager page that integrates
55
* ObjectManager and FieldDesigner from @object-ui/plugin-designer.
6+
* Covers list view, detail view with URL-based navigation, and field management.
67
*/
78

89
import { describe, it, expect, vi, beforeEach } from 'vitest';
@@ -19,6 +20,7 @@ vi.mock('../context/MetadataProvider', () => ({
1920
label: 'Accounts',
2021
icon: 'Building',
2122
description: 'Customer accounts',
23+
enabled: true,
2224
fields: [
2325
{ name: 'id', type: 'text', label: 'ID', readonly: true },
2426
{ name: 'name', type: 'text', label: 'Account Name', required: true },
@@ -66,7 +68,9 @@ function renderPage(route = '/system/objects') {
6668
<MemoryRouter initialEntries={[route]}>
6769
<Routes>
6870
<Route path="/system/objects" element={<ObjectManagerPage />} />
71+
<Route path="/system/objects/:objectName" element={<ObjectManagerPage />} />
6972
<Route path="/apps/:appName/system/objects" element={<ObjectManagerPage />} />
73+
<Route path="/apps/:appName/system/objects/:objectName" element={<ObjectManagerPage />} />
7074
</Routes>
7175
</MemoryRouter>
7276
);
@@ -77,7 +81,7 @@ describe('ObjectManagerPage', () => {
7781
vi.clearAllMocks();
7882
});
7983

80-
describe('Rendering', () => {
84+
describe('List View', () => {
8185
it('should render the page with Object Manager title', () => {
8286
renderPage();
8387
const titles = screen.getAllByText('Object Manager');
@@ -105,61 +109,64 @@ describe('ObjectManagerPage', () => {
105109
});
106110
});
107111

108-
describe('Object Selection & Field Designer', () => {
109-
it('should show FieldDesigner when an object row is clicked', async () => {
110-
renderPage();
112+
describe('Detail View (URL-based)', () => {
113+
it('should show object detail page when navigating to /system/objects/:objectName', () => {
114+
renderPage('/system/objects/account');
115+
expect(screen.getByTestId('object-detail-view')).toBeDefined();
116+
const titles = screen.getAllByText('Accounts');
117+
expect(titles.length).toBeGreaterThanOrEqual(1);
118+
});
111119

112-
// ObjectGrid renders data asynchronously via ValueDataSource.
113-
// Once loaded, primary field values become clickable links (data-testid="primary-field-link").
114-
await waitFor(() => {
115-
const links = screen.queryAllByTestId('primary-field-link');
116-
if (links.length > 0) {
117-
// Click the first object's primary field link
118-
fireEvent.click(links[0]);
119-
}
120-
});
120+
it('should show object properties section', () => {
121+
renderPage('/system/objects/account');
122+
expect(screen.getByTestId('object-properties')).toBeDefined();
123+
expect(screen.getByText('API Name')).toBeDefined();
124+
expect(screen.getByText('account')).toBeDefined();
125+
});
121126

122-
// Either FieldDesigner shows (if primary-field-link was found) or ObjectManager remains
123-
const fieldDesigner = screen.queryByTestId('field-designer');
124-
const objectManager = screen.queryByTestId('object-manager');
125-
expect(fieldDesigner || objectManager).toBeDefined();
127+
it('should show field management section with FieldDesigner', () => {
128+
renderPage('/system/objects/account');
129+
expect(screen.getByTestId('field-management-section')).toBeDefined();
130+
expect(screen.getByTestId('field-designer')).toBeDefined();
126131
});
127132

128-
it('should show back button when a field designer is active', async () => {
129-
renderPage();
133+
it('should show back button to return to object list', () => {
134+
renderPage('/system/objects/account');
135+
expect(screen.getByTestId('back-to-objects')).toBeDefined();
136+
});
130137

138+
it('should navigate back to object list when back button is clicked', async () => {
139+
renderPage('/system/objects/account');
140+
const backBtn = screen.getByTestId('back-to-objects');
141+
fireEvent.click(backBtn);
131142
await waitFor(() => {
132-
const links = screen.queryAllByTestId('primary-field-link');
133-
if (links.length > 0) {
134-
fireEvent.click(links[0]);
135-
}
143+
expect(screen.getByTestId('object-manager')).toBeDefined();
136144
});
145+
});
137146

138-
// If field designer is active, back button should be present
139-
const backBtn = screen.queryByTestId('back-to-objects');
140-
const fieldDesigner = screen.queryByTestId('field-designer');
141-
if (fieldDesigner) {
142-
expect(backBtn).toBeDefined();
143-
}
147+
it('should show relationships if the object has them', () => {
148+
renderPage('/system/objects/account');
149+
expect(screen.getByText('Relationships')).toBeDefined();
150+
expect(screen.getByText(/contact.*one-to-many/)).toBeDefined();
144151
});
152+
});
145153

146-
it('should return to object list when back button is clicked', async () => {
154+
describe('Object Selection via ObjectGrid', () => {
155+
it('should navigate to detail when primary field link is clicked', async () => {
147156
renderPage();
148157

158+
// ObjectGrid renders data asynchronously. Primary field links are auto-generated.
149159
await waitFor(() => {
150160
const links = screen.queryAllByTestId('primary-field-link');
151161
if (links.length > 0) {
152162
fireEvent.click(links[0]);
153163
}
154164
});
155165

156-
const backBtn = screen.queryByTestId('back-to-objects');
157-
if (backBtn) {
158-
fireEvent.click(backBtn);
159-
await waitFor(() => {
160-
expect(screen.getByTestId('object-manager')).toBeDefined();
161-
});
162-
}
166+
// Either detail view shows (if link was found) or list remains
167+
const detailView = screen.queryByTestId('object-detail-view');
168+
const objectManager = screen.queryByTestId('object-manager');
169+
expect(detailView || objectManager).toBeDefined();
163170
});
164171
});
165172
});

0 commit comments

Comments
 (0)