Skip to content

Commit f06cdc8

Browse files
authored
Merge pull request #1136 from objectstack-ai/copilot/implement-object-manager-field-designer
2 parents e0317e4 + 324c5b5 commit f06cdc8

31 files changed

+3057
-3
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Object Manager visual designer** (`@object-ui/plugin-designer`): Enterprise-grade object management interface for creating, editing, deleting, and configuring meta-object definitions. Uses standard ObjectGrid for the list view and ModalForm for create/edit operations. Features include property editing (name, label, plural label, description, icon, group, sort order, enabled toggle), object relationship display, search/filter, system object protection, confirm dialogs for destructive actions, and read-only mode. 18 unit tests.
13+
14+
- **Field Designer visual designer** (`@object-ui/plugin-designer`): Enterprise-grade field configuration wizard supporting 27 field types with full CRUD operations. Uses standard ObjectGrid for the list view with a specialized FieldEditor panel for advanced type-specific properties. Features include uniqueness constraints, default values, picklist/option set management, read-only, hidden, validation rules (min/max/length/pattern/custom), external ID, history tracking, and database indexing. Type-specific editors for lookup references, formula expressions, and select options. Field type filtering, search, system field protection, and read-only mode. 22 unit tests.
15+
16+
- **New type definitions** (`@object-ui/types`): Added `ObjectDefinition`, `ObjectDefinitionRelationship`, `ObjectManagerSchema`, `DesignerFieldType` (27 field types), `DesignerFieldOption`, `DesignerValidationRule`, `DesignerFieldDefinition`, and `FieldDesignerSchema` interfaces for the Object Manager and Field Designer components.
17+
18+
- **New i18n keys for Object Manager and Field Designer** (`@object-ui/i18n`): Added 50 new translation keys per locale across all 10 locale packs (en, zh, ja, ko, de, fr, es, pt, ru, ar) covering both `objectManager` and `fieldDesigner` subsections of `appDesigner`.
19+
20+
- **Designer translation fallbacks** (`@object-ui/plugin-designer`): Updated `useDesignerTranslation` with fallback translations for all new Object Manager and Field Designer keys.
21+
22+
- **Console integration** (`@object-ui/console`): Object Manager and Field Designer are now accessible in the console application at `/system/objects`. Added ObjectManagerPage to system admin routes, SystemHubPage card, and sidebar navigation. Selecting an object drills into its FieldDesigner for field configuration. 7 unit tests.
23+
1024
### Changed
1125

1226
- **System settings pages refactored to ObjectView** (`apps/console`): All five system management pages (Users, Organizations, Roles, Permissions, Audit Log) now use the metadata-driven `ObjectView` from `@object-ui/plugin-view` instead of hand-written HTML tables. Each page's UI is driven by the object definitions in `systemObjects.ts`, providing automatic search, sort, filter, and CRUD capabilities. A shared `SystemObjectViewPage` component eliminates code duplication across all system pages.
1327

1428
### Fixed
1529

30+
- **Plugin designer test infrastructure** (`@object-ui/plugin-designer`): Created missing `vitest.setup.ts` with ResizeObserver polyfill and jest-dom matchers. Added `@object-ui/i18n` alias to vite config. These fixes resolved 9 pre-existing test suite failures, bringing total passing tests from 45 to 246.
31+
1632
- **Chinese language pack (zh.ts) untranslated key** (`@object-ui/i18n`): Fixed `console.objectView.toolbarEnabledCount` which was still in English (`'{{count}} of {{total}} enabled'`) — now properly translated to `'已启用 {{count}}/{{total}} 项'`. Also fixed the same untranslated key in all other 8 non-English locales (ja, ko, de, fr, es, pt, ru, ar).
1733

1834
- **Hardcoded English strings in platform UI** (`apps/console`, `@object-ui/fields`, `@object-ui/react`, `@object-ui/components`): Replaced hardcoded English strings with i18n `t()` calls:

ROADMAP.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
> **Spec Version:** @objectstack/spec v3.3.0
66
> **Client Version:** @objectstack/client v3.3.0
77
> **Target UX Benchmark:** 🎯 Airtable parity
8-
> **Current Priority:** AppShell Navigation · Designer Interaction · **View Config Live Preview Sync ✅** · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** · **System Settings & App Management ✅** · **Right-Side Visual Editor Drawer ✅**
8+
> **Current Priority:** AppShell Navigation · Designer Interaction · **View Config Live Preview Sync ✅** · Dashboard Config Panel · Airtable UX Polish · **Flow Designer ✅** · **App Creation & Editing Flow ✅** · **System Settings & App Management ✅** · **Right-Side Visual Editor Drawer ✅** · **Object Manager & Field Designer ✅**
99
1010
---
1111

@@ -876,6 +876,45 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
876876
- [x] 10 unit tests for `useObjectLabel` hook
877877
- [x] Zero changes to object metadata files or translation files
878878

879+
### P1.16 Object Manager & Field Designer ✅
880+
881+
> **Status:** Complete — `ObjectManager` and `FieldDesigner` components shipped in `@object-ui/plugin-designer`.
882+
883+
Enterprise-grade visual designers for managing object definitions and configuring fields. Supports the full metadata platform workflow: define objects, configure fields with advanced properties, and maintain relationships.
884+
885+
**Object Manager (`ObjectManager`):**
886+
- [x] CRUD operations on object definitions (custom and system objects)
887+
- [x] Visual configuration of object properties (name, label, plural label, description, icon, group, sort order, enabled)
888+
- [x] Object relationship display and maintenance
889+
- [x] Inline property editor with collapsible sections
890+
- [x] Search/filter functionality
891+
- [x] Grouped object display with badges
892+
- [x] System object protection (non-deletable, name-locked)
893+
- [x] Read-only mode support
894+
- [x] Confirm dialog for destructive actions
895+
- [x] 18 unit tests
896+
897+
**Field Designer (`FieldDesigner`):**
898+
- [x] CRUD operations on field definitions with 27 supported field types
899+
- [x] Advanced field properties: uniqueness, default values, options/picklists, read-only, hidden, validation rules, external ID, history tracking, indexed
900+
- [x] Field grouping, sorting, and layout management
901+
- [x] System reserved field protection
902+
- [x] Type-specific property editors (lookup reference, formula expression, select options)
903+
- [x] Validation rule builder (min, max, minLength, maxLength, pattern, custom)
904+
- [x] Search and type-based filtering
905+
- [x] Read-only mode support
906+
- [x] 22 unit tests
907+
908+
**Type Definitions (`@object-ui/types`):**
909+
- [x] `ObjectDefinition`, `ObjectDefinitionRelationship`, `ObjectManagerSchema`
910+
- [x] `DesignerFieldType` (27 types), `DesignerFieldOption`, `DesignerValidationRule`
911+
- [x] `DesignerFieldDefinition`, `FieldDesignerSchema`
912+
913+
**i18n Support:**
914+
- [x] Full translations for all 10 locales (en, zh, ja, ko, de, fr, es, pt, ru, ar)
915+
- [x] 50 new translation keys per locale (objectManager + fieldDesigner sections)
916+
- [x] Fallback translations in `useDesignerTranslation` for standalone usage
917+
879918
---
880919

881920
## 🧩 P2 — Polish & Advanced Features

apps/console/src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const ForgotPasswordPage = lazy(() => import('./pages/ForgotPasswordPage').then(
4747
// System Admin Pages (lazy — rarely accessed)
4848
const SystemHubPage = lazy(() => import('./pages/system/SystemHubPage').then(m => ({ default: m.SystemHubPage })));
4949
const AppManagementPage = lazy(() => import('./pages/system/AppManagementPage').then(m => ({ default: m.AppManagementPage })));
50+
const ObjectManagerPage = lazy(() => import('./pages/system/ObjectManagerPage').then(m => ({ default: m.ObjectManagerPage })));
5051
const UserManagementPage = lazy(() => import('./pages/system/UserManagementPage').then(m => ({ default: m.UserManagementPage })));
5152
const OrgManagementPage = lazy(() => import('./pages/system/OrgManagementPage').then(m => ({ default: m.OrgManagementPage })));
5253
const RoleManagementPage = lazy(() => import('./pages/system/RoleManagementPage').then(m => ({ default: m.RoleManagementPage })));
@@ -287,6 +288,8 @@ export function AppContent() {
287288
<Route path="create-app" element={<CreateAppPage />} />
288289
<Route path="system" element={<SystemHubPage />} />
289290
<Route path="system/apps" element={<AppManagementPage />} />
291+
<Route path="system/objects" element={<ObjectManagerPage />} />
292+
<Route path="system/objects/:objectName" element={<ObjectManagerPage />} />
290293
<Route path="system/users" element={<UserManagementPage />} />
291294
<Route path="system/organizations" element={<OrgManagementPage />} />
292295
<Route path="system/roles" element={<RoleManagementPage />} />
@@ -379,6 +382,8 @@ export function AppContent() {
379382
{/* System Administration Routes */}
380383
<Route path="system" element={<SystemHubPage />} />
381384
<Route path="system/apps" element={<AppManagementPage />} />
385+
<Route path="system/objects" element={<ObjectManagerPage />} />
386+
<Route path="system/objects/:objectName" element={<ObjectManagerPage />} />
382387
<Route path="system/users" element={<UserManagementPage />} />
383388
<Route path="system/organizations" element={<OrgManagementPage />} />
384389
<Route path="system/roles" element={<RoleManagementPage />} />
@@ -501,6 +506,8 @@ function SystemRoutes() {
501506
<Routes>
502507
<Route path="/" element={<SystemHubPage />} />
503508
<Route path="apps" element={<AppManagementPage />} />
509+
<Route path="objects" element={<ObjectManagerPage />} />
510+
<Route path="objects/:objectName" element={<ObjectManagerPage />} />
504511
<Route path="users" element={<UserManagementPage />} />
505512
<Route path="organizations" element={<OrgManagementPage />} />
506513
<Route path="roles" element={<RoleManagementPage />} />
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* ObjectManagerPage tests
3+
*
4+
* Tests for the system administration Object Manager page that integrates
5+
* ObjectManager and FieldDesigner from @object-ui/plugin-designer.
6+
* Covers list view, detail view with URL-based navigation, and field management.
7+
*/
8+
9+
import { describe, it, expect, vi, beforeEach } from 'vitest';
10+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
11+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
12+
import { ObjectManagerPage } from '../pages/system/ObjectManagerPage';
13+
14+
// Mock MetadataProvider
15+
vi.mock('../context/MetadataProvider', () => ({
16+
useMetadata: () => ({
17+
objects: [
18+
{
19+
name: 'account',
20+
label: 'Accounts',
21+
icon: 'Building',
22+
description: 'Customer accounts',
23+
enabled: true,
24+
fields: [
25+
{ name: 'id', type: 'text', label: 'ID', readonly: true },
26+
{ name: 'name', type: 'text', label: 'Account Name', required: true },
27+
{ name: 'email', type: 'email', label: 'Email' },
28+
{ name: 'status', type: 'select', label: 'Status', options: ['active', 'inactive'] },
29+
],
30+
relationships: [
31+
{ object: 'contact', type: 'one-to-many', name: 'contacts' },
32+
],
33+
},
34+
{
35+
name: 'contact',
36+
label: 'Contacts',
37+
icon: 'Users',
38+
fields: [
39+
{ name: 'id', type: 'text', label: 'ID', readonly: true },
40+
{ name: 'name', type: 'text', label: 'Name', required: true },
41+
],
42+
},
43+
{
44+
name: 'sys_user',
45+
label: 'Users',
46+
icon: 'Users',
47+
fields: [
48+
{ name: 'id', type: 'text', label: 'ID', readonly: true },
49+
{ name: 'name', type: 'text', label: 'Name', required: true },
50+
{ name: 'email', type: 'email', label: 'Email', required: true },
51+
],
52+
},
53+
],
54+
refresh: vi.fn(),
55+
}),
56+
}));
57+
58+
// Mock sonner toast
59+
vi.mock('sonner', () => ({
60+
toast: {
61+
success: vi.fn(),
62+
error: vi.fn(),
63+
},
64+
}));
65+
66+
function renderPage(route = '/system/objects') {
67+
return render(
68+
<MemoryRouter initialEntries={[route]}>
69+
<Routes>
70+
<Route path="/system/objects" element={<ObjectManagerPage />} />
71+
<Route path="/system/objects/:objectName" element={<ObjectManagerPage />} />
72+
<Route path="/apps/:appName/system/objects" element={<ObjectManagerPage />} />
73+
<Route path="/apps/:appName/system/objects/:objectName" element={<ObjectManagerPage />} />
74+
</Routes>
75+
</MemoryRouter>
76+
);
77+
}
78+
79+
describe('ObjectManagerPage', () => {
80+
beforeEach(() => {
81+
vi.clearAllMocks();
82+
});
83+
84+
describe('List View', () => {
85+
it('should render the page with Object Manager title', () => {
86+
renderPage();
87+
const titles = screen.getAllByText('Object Manager');
88+
expect(titles.length).toBeGreaterThanOrEqual(1);
89+
expect(screen.getByText('Manage object definitions and field configurations')).toBeDefined();
90+
});
91+
92+
it('should render the page container', () => {
93+
renderPage();
94+
expect(screen.getByTestId('object-manager-page')).toBeDefined();
95+
});
96+
97+
it('should render the ObjectManager component with objects from metadata', () => {
98+
renderPage();
99+
expect(screen.getByTestId('object-manager')).toBeDefined();
100+
});
101+
102+
it('should display metadata objects via ObjectGrid', async () => {
103+
renderPage();
104+
// ObjectGrid (from plugin-grid) renders the data asynchronously via ValueDataSource
105+
await waitFor(() => {
106+
const content = screen.getByTestId('object-manager').textContent;
107+
expect(content).toBeDefined();
108+
});
109+
});
110+
});
111+
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+
});
119+
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+
});
126+
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();
131+
});
132+
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+
});
137+
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);
142+
await waitFor(() => {
143+
expect(screen.getByTestId('object-manager')).toBeDefined();
144+
});
145+
});
146+
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();
151+
});
152+
});
153+
154+
describe('Object Selection via ObjectGrid', () => {
155+
it('should navigate to detail when primary field link is clicked', async () => {
156+
renderPage();
157+
158+
// ObjectGrid renders data asynchronously via ValueDataSource.
159+
// Wait for primary-field-link buttons to appear.
160+
await waitFor(() => {
161+
const links = screen.queryAllByTestId('primary-field-link');
162+
expect(links.length).toBeGreaterThan(0);
163+
}, { timeout: 5000 });
164+
165+
// Click the first primary field link (should be 'account')
166+
const links = screen.getAllByTestId('primary-field-link');
167+
fireEvent.click(links[0]);
168+
169+
// Should navigate to the object detail view
170+
await waitFor(() => {
171+
expect(screen.getByTestId('object-detail-view')).toBeDefined();
172+
});
173+
});
174+
});
175+
});

apps/console/src/components/AppSidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
257257
const systemFallbackNavigation: NavigationItem[] = React.useMemo(() => [
258258
{ id: 'sys-settings', label: 'System Settings', type: 'url' as const, url: '/system', icon: 'settings' },
259259
{ id: 'sys-apps', label: 'Applications', type: 'url' as const, url: '/system/apps', icon: 'layout-grid' },
260+
{ id: 'sys-objects', label: 'Object Manager', type: 'url' as const, url: '/system/objects', icon: 'database' },
260261
{ id: 'sys-users', label: 'Users', type: 'url' as const, url: '/system/users', icon: 'users' },
261262
{ id: 'sys-orgs', label: 'Organizations', type: 'url' as const, url: '/system/organizations', icon: 'building-2' },
262263
{ id: 'sys-roles', label: 'Roles', type: 'url' as const, url: '/system/roles', icon: 'shield' },

0 commit comments

Comments
 (0)