Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **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.

- **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.

- **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.

- **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`.

- **Designer translation fallbacks** (`@object-ui/plugin-designer`): Updated `useDesignerTranslation` with fallback translations for all new Object Manager and Field Designer keys.

- **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.

### Changed

- **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.

### Fixed

- **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.

- **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).

- **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:
Expand Down
41 changes: 40 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
> **Spec Version:** @objectstack/spec v3.3.0
> **Client Version:** @objectstack/client v3.3.0
> **Target UX Benchmark:** 🎯 Airtable parity
> **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 ✅**
> **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 ✅**

---

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

### P1.16 Object Manager & Field Designer ✅

> **Status:** Complete — `ObjectManager` and `FieldDesigner` components shipped in `@object-ui/plugin-designer`.

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.

**Object Manager (`ObjectManager`):**
- [x] CRUD operations on object definitions (custom and system objects)
- [x] Visual configuration of object properties (name, label, plural label, description, icon, group, sort order, enabled)
- [x] Object relationship display and maintenance
- [x] Inline property editor with collapsible sections
- [x] Search/filter functionality
- [x] Grouped object display with badges
- [x] System object protection (non-deletable, name-locked)
- [x] Read-only mode support
- [x] Confirm dialog for destructive actions
- [x] 18 unit tests

**Field Designer (`FieldDesigner`):**
- [x] CRUD operations on field definitions with 27 supported field types
- [x] Advanced field properties: uniqueness, default values, options/picklists, read-only, hidden, validation rules, external ID, history tracking, indexed
- [x] Field grouping, sorting, and layout management
- [x] System reserved field protection
- [x] Type-specific property editors (lookup reference, formula expression, select options)
- [x] Validation rule builder (min, max, minLength, maxLength, pattern, custom)
- [x] Search and type-based filtering
- [x] Read-only mode support
- [x] 22 unit tests

**Type Definitions (`@object-ui/types`):**
- [x] `ObjectDefinition`, `ObjectDefinitionRelationship`, `ObjectManagerSchema`
- [x] `DesignerFieldType` (27 types), `DesignerFieldOption`, `DesignerValidationRule`
- [x] `DesignerFieldDefinition`, `FieldDesignerSchema`

**i18n Support:**
- [x] Full translations for all 10 locales (en, zh, ja, ko, de, fr, es, pt, ru, ar)
- [x] 50 new translation keys per locale (objectManager + fieldDesigner sections)
- [x] Fallback translations in `useDesignerTranslation` for standalone usage

---

## 🧩 P2 — Polish & Advanced Features
Expand Down
7 changes: 7 additions & 0 deletions apps/console/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const ForgotPasswordPage = lazy(() => import('./pages/ForgotPasswordPage').then(
// System Admin Pages (lazy — rarely accessed)
const SystemHubPage = lazy(() => import('./pages/system/SystemHubPage').then(m => ({ default: m.SystemHubPage })));
const AppManagementPage = lazy(() => import('./pages/system/AppManagementPage').then(m => ({ default: m.AppManagementPage })));
const ObjectManagerPage = lazy(() => import('./pages/system/ObjectManagerPage').then(m => ({ default: m.ObjectManagerPage })));
const UserManagementPage = lazy(() => import('./pages/system/UserManagementPage').then(m => ({ default: m.UserManagementPage })));
const OrgManagementPage = lazy(() => import('./pages/system/OrgManagementPage').then(m => ({ default: m.OrgManagementPage })));
const RoleManagementPage = lazy(() => import('./pages/system/RoleManagementPage').then(m => ({ default: m.RoleManagementPage })));
Expand Down Expand Up @@ -287,6 +288,8 @@ export function AppContent() {
<Route path="create-app" element={<CreateAppPage />} />
<Route path="system" element={<SystemHubPage />} />
<Route path="system/apps" element={<AppManagementPage />} />
<Route path="system/objects" element={<ObjectManagerPage />} />
<Route path="system/objects/:objectName" element={<ObjectManagerPage />} />
<Route path="system/users" element={<UserManagementPage />} />
<Route path="system/organizations" element={<OrgManagementPage />} />
<Route path="system/roles" element={<RoleManagementPage />} />
Expand Down Expand Up @@ -379,6 +382,8 @@ export function AppContent() {
{/* System Administration Routes */}
<Route path="system" element={<SystemHubPage />} />
<Route path="system/apps" element={<AppManagementPage />} />
<Route path="system/objects" element={<ObjectManagerPage />} />
<Route path="system/objects/:objectName" element={<ObjectManagerPage />} />
<Route path="system/users" element={<UserManagementPage />} />
<Route path="system/organizations" element={<OrgManagementPage />} />
<Route path="system/roles" element={<RoleManagementPage />} />
Expand Down Expand Up @@ -501,6 +506,8 @@ function SystemRoutes() {
<Routes>
<Route path="/" element={<SystemHubPage />} />
<Route path="apps" element={<AppManagementPage />} />
<Route path="objects" element={<ObjectManagerPage />} />
<Route path="objects/:objectName" element={<ObjectManagerPage />} />
<Route path="users" element={<UserManagementPage />} />
<Route path="organizations" element={<OrgManagementPage />} />
<Route path="roles" element={<RoleManagementPage />} />
Expand Down
175 changes: 175 additions & 0 deletions apps/console/src/__tests__/ObjectManagerPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* ObjectManagerPage tests
*
* Tests for the system administration Object Manager page that integrates
* ObjectManager and FieldDesigner from @object-ui/plugin-designer.
* Covers list view, detail view with URL-based navigation, and field management.
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { ObjectManagerPage } from '../pages/system/ObjectManagerPage';

// Mock MetadataProvider
vi.mock('../context/MetadataProvider', () => ({
useMetadata: () => ({
objects: [
{
name: 'account',
label: 'Accounts',
icon: 'Building',
description: 'Customer accounts',
enabled: true,
fields: [
{ name: 'id', type: 'text', label: 'ID', readonly: true },
{ name: 'name', type: 'text', label: 'Account Name', required: true },
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'status', type: 'select', label: 'Status', options: ['active', 'inactive'] },
],
relationships: [
{ object: 'contact', type: 'one-to-many', name: 'contacts' },
],
},
{
name: 'contact',
label: 'Contacts',
icon: 'Users',
fields: [
{ name: 'id', type: 'text', label: 'ID', readonly: true },
{ name: 'name', type: 'text', label: 'Name', required: true },
],
},
{
name: 'sys_user',
label: 'Users',
icon: 'Users',
fields: [
{ name: 'id', type: 'text', label: 'ID', readonly: true },
{ name: 'name', type: 'text', label: 'Name', required: true },
{ name: 'email', type: 'email', label: 'Email', required: true },
],
},
],
refresh: vi.fn(),
}),
}));

// Mock sonner toast
vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
},
}));

function renderPage(route = '/system/objects') {
return render(
<MemoryRouter initialEntries={[route]}>
<Routes>
<Route path="/system/objects" element={<ObjectManagerPage />} />
<Route path="/system/objects/:objectName" element={<ObjectManagerPage />} />
<Route path="/apps/:appName/system/objects" element={<ObjectManagerPage />} />
<Route path="/apps/:appName/system/objects/:objectName" element={<ObjectManagerPage />} />
</Routes>
</MemoryRouter>
);
}

describe('ObjectManagerPage', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('List View', () => {
it('should render the page with Object Manager title', () => {
renderPage();
const titles = screen.getAllByText('Object Manager');
expect(titles.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('Manage object definitions and field configurations')).toBeDefined();
});

it('should render the page container', () => {
renderPage();
expect(screen.getByTestId('object-manager-page')).toBeDefined();
});

it('should render the ObjectManager component with objects from metadata', () => {
renderPage();
expect(screen.getByTestId('object-manager')).toBeDefined();
});

it('should display metadata objects via ObjectGrid', async () => {
renderPage();
// ObjectGrid (from plugin-grid) renders the data asynchronously via ValueDataSource
await waitFor(() => {
const content = screen.getByTestId('object-manager').textContent;
expect(content).toBeDefined();
});
});
});

describe('Detail View (URL-based)', () => {
it('should show object detail page when navigating to /system/objects/:objectName', () => {
renderPage('/system/objects/account');
expect(screen.getByTestId('object-detail-view')).toBeDefined();
const titles = screen.getAllByText('Accounts');
expect(titles.length).toBeGreaterThanOrEqual(1);
});

it('should show object properties section', () => {
renderPage('/system/objects/account');
expect(screen.getByTestId('object-properties')).toBeDefined();
expect(screen.getByText('API Name')).toBeDefined();
expect(screen.getByText('account')).toBeDefined();
});

it('should show field management section with FieldDesigner', () => {
renderPage('/system/objects/account');
expect(screen.getByTestId('field-management-section')).toBeDefined();
expect(screen.getByTestId('field-designer')).toBeDefined();
});

it('should show back button to return to object list', () => {
renderPage('/system/objects/account');
expect(screen.getByTestId('back-to-objects')).toBeDefined();
});

it('should navigate back to object list when back button is clicked', async () => {
renderPage('/system/objects/account');
const backBtn = screen.getByTestId('back-to-objects');
fireEvent.click(backBtn);
await waitFor(() => {
expect(screen.getByTestId('object-manager')).toBeDefined();
});
});

it('should show relationships if the object has them', () => {
renderPage('/system/objects/account');
expect(screen.getByText('Relationships')).toBeDefined();
expect(screen.getByText(/contact.*one-to-many/)).toBeDefined();
});
});

describe('Object Selection via ObjectGrid', () => {
it('should navigate to detail when primary field link is clicked', async () => {
renderPage();

// ObjectGrid renders data asynchronously via ValueDataSource.
// Wait for primary-field-link buttons to appear.
await waitFor(() => {
const links = screen.queryAllByTestId('primary-field-link');
expect(links.length).toBeGreaterThan(0);
}, { timeout: 5000 });

// Click the first primary field link (should be 'account')
const links = screen.getAllByTestId('primary-field-link');
fireEvent.click(links[0]);

// Should navigate to the object detail view
await waitFor(() => {
expect(screen.getByTestId('object-detail-view')).toBeDefined();
});
});
});
});
1 change: 1 addition & 0 deletions apps/console/src/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
const systemFallbackNavigation: NavigationItem[] = React.useMemo(() => [
{ id: 'sys-settings', label: 'System Settings', type: 'url' as const, url: '/system', icon: 'settings' },
{ id: 'sys-apps', label: 'Applications', type: 'url' as const, url: '/system/apps', icon: 'layout-grid' },
{ id: 'sys-objects', label: 'Object Manager', type: 'url' as const, url: '/system/objects', icon: 'database' },
{ id: 'sys-users', label: 'Users', type: 'url' as const, url: '/system/users', icon: 'users' },
{ id: 'sys-orgs', label: 'Organizations', type: 'url' as const, url: '/system/organizations', icon: 'building-2' },
{ id: 'sys-roles', label: 'Roles', type: 'url' as const, url: '/system/roles', icon: 'shield' },
Expand Down
Loading