diff --git a/CHANGELOG.md b/CHANGELOG.md index 742068e90..29f560d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 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 - **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). diff --git a/ROADMAP.md b/ROADMAP.md index 9dd1bfffb..0841f6a57 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -757,6 +757,17 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - [x] Search/filter permissions - [x] Admin-only create/delete controls +**ObjectView-Driven System Pages (P1.12.2):** +- [x] Shared `SystemObjectViewPage` component using `ObjectView` from `@object-ui/plugin-view` +- [x] User Management (`/system/users`) driven by `sys_user` metadata via ObjectView +- [x] Organization Management (`/system/organizations`) driven by `sys_org` metadata via ObjectView +- [x] Role Management (`/system/roles`) driven by `sys_role` metadata via ObjectView +- [x] Permission Management (`/system/permissions`) driven by `sys_permission` metadata via ObjectView +- [x] Audit Log (`/system/audit-log`) driven by `sys_audit_log` metadata via ObjectView (read-only) +- [x] Admin-only CRUD operations controlled via ObjectView `operations` config +- [x] Automatic search, sort, filter, pagination from ObjectView capabilities +- [x] 22 system page tests passing + **Sidebar & Navigation Updates:** - [x] Settings button → `/system/` hub (was `/system/profile`) - [x] App switcher "Manage All Apps" link → `/system/apps` diff --git a/apps/console/src/__tests__/SystemPages.test.tsx b/apps/console/src/__tests__/SystemPages.test.tsx index 595b05fa9..adc6a8d36 100644 --- a/apps/console/src/__tests__/SystemPages.test.tsx +++ b/apps/console/src/__tests__/SystemPages.test.tsx @@ -1,8 +1,9 @@ /** * System Admin Pages Integration Tests * - * Tests that system pages (User, Org, Role, AuditLog) fetch data - * via useAdapter() and render records from the API. + * Tests that system pages render the correct page header and delegate + * data rendering to the ObjectView component from @object-ui/plugin-view, + * configured with the appropriate object metadata from systemObjects.ts. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -10,19 +11,38 @@ import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { MemoryRouter } from 'react-router-dom'; +// --- Capture ObjectView props for assertion --- +let lastObjectViewProps: any = null; + +vi.mock('@object-ui/plugin-view', () => ({ + ObjectView: (props: any) => { + lastObjectViewProps = props; + return ( +
+ ); + }, +})); + // --- Shared mock adapter --- const mockFind = vi.fn().mockResolvedValue({ data: [], total: 0 }); const mockCreate = vi.fn().mockResolvedValue({ id: 'new-1' }); const mockDelete = vi.fn().mockResolvedValue({}); +const mockAdapter = { + find: mockFind, + create: mockCreate, + delete: mockDelete, + update: vi.fn(), + findOne: vi.fn(), + getObjectSchema: vi.fn().mockResolvedValue({ name: 'test', fields: {} }), +}; + vi.mock('../context/AdapterProvider', () => ({ - useAdapter: () => ({ - find: mockFind, - create: mockCreate, - delete: mockDelete, - update: vi.fn(), - findOne: vi.fn(), - }), + useAdapter: () => mockAdapter, })); vi.mock('@object-ui/auth', () => ({ @@ -75,110 +95,88 @@ function wrap(ui: React.ReactElement) { beforeEach(() => { vi.clearAllMocks(); + lastObjectViewProps = null; }); describe('UserManagementPage', () => { - it('should call dataSource.find("sys_user") on mount', async () => { - mockFind.mockResolvedValueOnce({ - data: [{ id: '1', name: 'Alice', email: 'alice@test.com', role: 'admin', status: 'active', lastLoginAt: '' }], - }); + it('should render ObjectView with sys_user object and page header', () => { wrap(); - await waitFor(() => { - expect(mockFind).toHaveBeenCalledWith('sys_user'); - }); - await waitFor(() => { - expect(screen.getByText('Alice')).toBeInTheDocument(); - }); + expect(screen.getByText('User Management')).toBeInTheDocument(); + expect(screen.getByText('Manage system users and their roles')).toBeInTheDocument(); + expect(screen.getByTestId('plugin-object-view')).toBeInTheDocument(); + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_user'); }); - it('should show empty state when no users', async () => { - mockFind.mockResolvedValueOnce({ data: [] }); + it('should pass the adapter as dataSource to ObjectView', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No users found.')).toBeInTheDocument(); - }); + expect(lastObjectViewProps.dataSource).toBe(mockAdapter); }); - it('should call create when Add User is clicked', async () => { - mockFind.mockResolvedValue({ data: [] }); - mockCreate.mockResolvedValueOnce({ id: 'new-user' }); + it('should enable CRUD operations for admin users', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No users found.')).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText('Add User')); - await waitFor(() => { - expect(mockCreate).toHaveBeenCalledWith('sys_user', expect.objectContaining({ name: 'New User' })); - }); + const ops = lastObjectViewProps.schema.operations; + expect(ops).toEqual({ create: true, update: true, delete: true }); + }); + + it('should configure table columns from systemObjects metadata', () => { + wrap(); + expect(lastObjectViewProps.schema.table.columns).toEqual( + ['name', 'email', 'role', 'status', 'lastLoginAt'] + ); }); }); describe('OrgManagementPage', () => { - it('should call dataSource.find("sys_org") on mount', async () => { - mockFind.mockResolvedValueOnce({ - data: [{ id: '1', name: 'Acme', slug: 'acme', plan: 'pro', status: 'active', memberCount: 5 }], - }); + it('should render ObjectView with sys_org object and page header', () => { wrap(); - await waitFor(() => { - expect(mockFind).toHaveBeenCalledWith('sys_org'); - }); - await waitFor(() => { - expect(screen.getByText('Acme')).toBeInTheDocument(); - }); + expect(screen.getByText('Organization Management')).toBeInTheDocument(); + expect(screen.getByText('Manage organizations and their members')).toBeInTheDocument(); + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_org'); }); - it('should show empty state when no organizations', async () => { - mockFind.mockResolvedValueOnce({ data: [] }); + it('should configure table columns from systemObjects metadata', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No organizations found.')).toBeInTheDocument(); - }); + expect(lastObjectViewProps.schema.table.columns).toEqual( + ['name', 'slug', 'plan', 'status', 'memberCount'] + ); }); }); describe('RoleManagementPage', () => { - it('should call dataSource.find("sys_role") on mount', async () => { - mockFind.mockResolvedValueOnce({ - data: [{ id: '1', name: 'Admin', description: 'Full access', isSystem: true, userCount: 3 }], - }); + it('should render ObjectView with sys_role object and page header', () => { wrap(); - await waitFor(() => { - expect(mockFind).toHaveBeenCalledWith('sys_role'); - }); - await waitFor(() => { - expect(screen.getByText('Admin')).toBeInTheDocument(); - }); + expect(screen.getByText('Role Management')).toBeInTheDocument(); + expect(screen.getByText('Define roles and assign permissions')).toBeInTheDocument(); + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_role'); }); - it('should show empty state when no roles', async () => { - mockFind.mockResolvedValueOnce({ data: [] }); + it('should configure table columns from systemObjects metadata', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No roles found.')).toBeInTheDocument(); - }); + expect(lastObjectViewProps.schema.table.columns).toEqual( + ['name', 'description', 'isSystem', 'userCount'] + ); }); }); describe('AuditLogPage', () => { - it('should call dataSource.find("sys_audit_log") on mount', async () => { - mockFind.mockResolvedValueOnce({ - data: [{ id: '1', action: 'create', resource: 'user', userName: 'Admin', ipAddress: '127.0.0.1', createdAt: '2026-01-01' }], - }); + it('should render ObjectView with sys_audit_log object and page header', () => { wrap(); - await waitFor(() => { - expect(mockFind).toHaveBeenCalledWith('sys_audit_log', expect.objectContaining({ $orderby: { createdAt: 'desc' } })); - }); - await waitFor(() => { - expect(screen.getByText('create')).toBeInTheDocument(); - }); + expect(screen.getByText('Audit Log')).toBeInTheDocument(); + expect(screen.getByText('View system activity and user actions')).toBeInTheDocument(); + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_audit_log'); }); - it('should show empty state when no logs', async () => { - mockFind.mockResolvedValueOnce({ data: [] }); + it('should disable all mutation operations (read-only)', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No audit logs found.')).toBeInTheDocument(); - }); + const ops = lastObjectViewProps.schema.operations; + expect(ops).toEqual({ create: false, update: false, delete: false }); + }); + + it('should configure table columns from systemObjects metadata', () => { + wrap(); + expect(lastObjectViewProps.schema.table.columns).toEqual( + ['action', 'resource', 'userName', 'ipAddress', 'createdAt'] + ); }); }); @@ -250,56 +248,29 @@ describe('AppManagementPage', () => { }); describe('PermissionManagementPage', () => { - it('should call dataSource.find("sys_permission") on mount', async () => { - mockFind.mockResolvedValueOnce({ - data: [{ id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: 'Full user access' }], - }); + it('should render ObjectView with sys_permission object and page header', () => { wrap(); - await waitFor(() => { - expect(mockFind).toHaveBeenCalledWith('sys_permission'); - }); - await waitFor(() => { - expect(screen.getByText('manage_users')).toBeInTheDocument(); - }); + expect(screen.getByText('Permissions')).toBeInTheDocument(); + expect(screen.getByText('Manage permission rules and assignments')).toBeInTheDocument(); + expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_permission'); }); - it('should show empty state when no permissions', async () => { - mockFind.mockResolvedValueOnce({ data: [] }); + it('should enable CRUD operations for admin users', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No permissions found.')).toBeInTheDocument(); - }); + const ops = lastObjectViewProps.schema.operations; + expect(ops).toEqual({ create: true, update: true, delete: true }); }); - it('should call create when Add Permission is clicked', async () => { - mockFind.mockResolvedValue({ data: [] }); - mockCreate.mockResolvedValueOnce({ id: 'new-perm' }); + it('should configure table columns from systemObjects metadata', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('No permissions found.')).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText('Add Permission')); - await waitFor(() => { - expect(mockCreate).toHaveBeenCalledWith('sys_permission', expect.objectContaining({ name: 'New Permission' })); - }); + expect(lastObjectViewProps.schema.table.columns).toEqual( + ['name', 'resource', 'action', 'description'] + ); }); - it('should filter permissions by search query', async () => { - mockFind.mockResolvedValue({ - data: [ - { id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: '' }, - { id: '2', name: 'read_reports', resource: 'report', action: 'read', description: '' }, - ], - }); + it('should enable search and filters', () => { wrap(); - await waitFor(() => { - expect(screen.getByText('manage_users')).toBeInTheDocument(); - expect(screen.getByText('read_reports')).toBeInTheDocument(); - }); - fireEvent.change(screen.getByTestId('permission-search-input'), { target: { value: 'report' } }); - await waitFor(() => { - expect(screen.queryByText('manage_users')).not.toBeInTheDocument(); - expect(screen.getByText('read_reports')).toBeInTheDocument(); - }); + expect(lastObjectViewProps.schema.showSearch).toBe(true); + expect(lastObjectViewProps.schema.showFilters).toBe(true); }); }); diff --git a/apps/console/src/pages/system/AuditLogPage.tsx b/apps/console/src/pages/system/AuditLogPage.tsx index 6dcd3b57b..9e3e934e6 100644 --- a/apps/console/src/pages/system/AuditLogPage.tsx +++ b/apps/console/src/pages/system/AuditLogPage.tsx @@ -1,102 +1,23 @@ /** * Audit Log Page * - * Read-only grid displaying system audit logs. - * Fetches data via dataSource.find('sys_audit_log'). + * Read-only metadata-driven audit log powered by ObjectView. + * The sys_audit_log object definition in systemObjects.ts drives + * the table columns, search, and sort. All mutation operations + * are disabled (read-only view). */ -import { useState, useEffect, useCallback } from 'react'; -import { Card, CardContent, Badge } from '@object-ui/components'; -import { ScrollText, Loader2 } from 'lucide-react'; -import { toast } from 'sonner'; -import { useAdapter } from '../../context/AdapterProvider'; -import { systemObjects } from './systemObjects'; - -const auditObject = systemObjects.find((o) => o.name === 'sys_audit_log')!; -const columns = auditObject.views[0].columns; +import { ScrollText } from 'lucide-react'; +import { SystemObjectViewPage } from './SystemObjectViewPage'; export function AuditLogPage() { - const dataSource = useAdapter(); - - const [records, setRecords] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchData = useCallback(async () => { - if (!dataSource) return; - setLoading(true); - try { - const result = await dataSource.find('sys_audit_log', { $orderby: { createdAt: 'desc' } }); - setRecords(result.data || []); - } catch { - toast.error('Failed to load audit logs'); - } finally { - setLoading(false); - } - }, [dataSource]); - - useEffect(() => { fetchData(); }, [fetchData]); - return ( -
-
-
- -
-
-

Audit Log

-

View system activity and user actions

-
-
- - - -
- - - - {columns.map((col) => { - const field = auditObject.fields.find((f) => f.name === col); - return ( - - ); - })} - - - - {loading ? ( - - - - ) : records.length === 0 ? ( - - - - ) : ( - records.map((record) => ( - - {columns.map((col) => ( - - ))} - - )) - )} - -
- {field?.label ?? col} -
- -
-
- -

No audit logs found.

- Read-only -
-
- {String(record[col] ?? '')} -
-
-
-
-
+ ); } diff --git a/apps/console/src/pages/system/OrgManagementPage.tsx b/apps/console/src/pages/system/OrgManagementPage.tsx index 6fd497c4e..6e6c80660 100644 --- a/apps/console/src/pages/system/OrgManagementPage.tsx +++ b/apps/console/src/pages/system/OrgManagementPage.tsx @@ -1,144 +1,26 @@ /** * Organization Management Page * - * Displays a list of organizations with member management. - * Fetches data via dataSource.find('sys_org') and supports - * create / delete operations. + * Metadata-driven organization management powered by ObjectView. + * The sys_org object definition in systemObjects.ts drives + * the table columns, search, sort, and CRUD operations. */ -import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@object-ui/auth'; -import { Button, Card, CardContent, Badge } from '@object-ui/components'; -import { Plus, Building2, Loader2, Trash2 } from 'lucide-react'; -import { toast } from 'sonner'; -import { useAdapter } from '../../context/AdapterProvider'; -import { systemObjects } from './systemObjects'; - -const orgObject = systemObjects.find((o) => o.name === 'sys_org')!; -const columns = orgObject.views[0].columns; +import { Building2 } from 'lucide-react'; +import { SystemObjectViewPage } from './SystemObjectViewPage'; export function OrgManagementPage() { const { user: currentUser } = useAuth(); const isAdmin = currentUser?.role === 'admin'; - const dataSource = useAdapter(); - - const [records, setRecords] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchData = useCallback(async () => { - if (!dataSource) return; - setLoading(true); - try { - const result = await dataSource.find('sys_org'); - setRecords(result.data || []); - } catch { - toast.error('Failed to load organizations'); - } finally { - setLoading(false); - } - }, [dataSource]); - - useEffect(() => { fetchData(); }, [fetchData]); - - const handleCreate = useCallback(async () => { - if (!dataSource) return; - try { - await dataSource.create('sys_org', { name: 'New Organization', slug: '', plan: 'free', status: 'active' }); - toast.success('Organization created'); - fetchData(); - } catch { - toast.error('Failed to create organization'); - } - }, [dataSource, fetchData]); - - const handleDelete = useCallback(async (id: string) => { - if (!dataSource) return; - try { - await dataSource.delete('sys_org', id); - toast.success('Organization deleted'); - fetchData(); - } catch { - toast.error('Failed to delete organization'); - } - }, [dataSource, fetchData]); return ( -
-
-
-
- -
-
-

Organization Management

-

Manage organizations and their members

-
-
- {isAdmin && ( - - )} -
- - - -
- - - - {columns.map((col) => { - const field = orgObject.fields.find((f) => f.name === col); - return ( - - ); - })} - {isAdmin && } - - - - {loading ? ( - - - - ) : records.length === 0 ? ( - - - - ) : ( - records.map((record) => ( - - {columns.map((col) => ( - - ))} - {isAdmin && ( - - )} - - )) - )} - -
- {field?.label ?? col} - Actions
- -
-
- -

No organizations found.

- plugin-grid powered -
-
- {String(record[col] ?? '')} - - -
-
-
-
-
+ ); } diff --git a/apps/console/src/pages/system/PermissionManagementPage.tsx b/apps/console/src/pages/system/PermissionManagementPage.tsx index b914ef855..6c6fb33cb 100644 --- a/apps/console/src/pages/system/PermissionManagementPage.tsx +++ b/apps/console/src/pages/system/PermissionManagementPage.tsx @@ -1,163 +1,26 @@ /** * Permission Management Page * - * Displays a grid of sys_permission records with CRUD capabilities, - * search filtering, and role assignment. + * Metadata-driven permission management powered by ObjectView. + * The sys_permission object definition in systemObjects.ts drives + * the table columns, search, sort, and CRUD operations. */ -import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@object-ui/auth'; -import { Button, Badge, Input } from '@object-ui/components'; -import { Plus, Key, Loader2, Trash2, Search } from 'lucide-react'; -import { toast } from 'sonner'; -import { useAdapter } from '../../context/AdapterProvider'; -import { systemObjects } from './systemObjects'; - -const permObject = systemObjects.find((o) => o.name === 'sys_permission')!; -const columns = permObject.views[0].columns; +import { Key } from 'lucide-react'; +import { SystemObjectViewPage } from './SystemObjectViewPage'; export function PermissionManagementPage() { const { user: currentUser } = useAuth(); const isAdmin = currentUser?.role === 'admin'; - const dataSource = useAdapter(); - - const [records, setRecords] = useState([]); - const [loading, setLoading] = useState(true); - const [searchQuery, setSearchQuery] = useState(''); - - const fetchData = useCallback(async () => { - if (!dataSource) return; - setLoading(true); - try { - const result = await dataSource.find('sys_permission'); - setRecords(result.data || []); - } catch { - toast.error('Failed to load permissions'); - } finally { - setLoading(false); - } - }, [dataSource]); - - useEffect(() => { fetchData(); }, [fetchData]); - - const handleCreate = useCallback(async () => { - if (!dataSource) return; - try { - await dataSource.create('sys_permission', { - name: 'New Permission', - description: '', - resource: '', - action: 'read', - }); - toast.success('Permission created'); - fetchData(); - } catch { - toast.error('Failed to create permission'); - } - }, [dataSource, fetchData]); - - const handleDelete = useCallback(async (id: string) => { - if (!dataSource) return; - try { - await dataSource.delete('sys_permission', id); - toast.success('Permission deleted'); - fetchData(); - } catch { - toast.error('Failed to delete permission'); - } - }, [dataSource, fetchData]); - - // Filter records by search query - const filteredRecords = records.filter((r) => { - if (!searchQuery) return true; - const q = searchQuery.toLowerCase(); - return ( - (r.name || '').toLowerCase().includes(q) || - (r.resource || '').toLowerCase().includes(q) || - (r.action || '').toLowerCase().includes(q) || - (r.description || '').toLowerCase().includes(q) - ); - }); return ( -
-
-
- -
-

Permissions

-

Manage permission rules and assignments

-
-
- {isAdmin && ( - - )} -
- - {/* Search */} -
- - ) => setSearchQuery(e.target.value)} - className="pl-8" - data-testid="permission-search-input" - /> -
- - {loading ? ( -
- - Loading permissions... -
- ) : filteredRecords.length === 0 ? ( -

No permissions found.

- ) : ( -
- - - - {columns.map((col: string) => ( - - ))} - {isAdmin && } - - - - {filteredRecords.map((rec: any) => ( - - {columns.map((col: string) => ( - - ))} - {isAdmin && ( - - )} - - ))} - -
- {permObject.fields.find((f: any) => f.name === col)?.label || col} - Actions
- {col === 'action' ? ( - {rec[col]} - ) : ( - {rec[col] ?? '—'} - )} - - -
-
- )} -
+ ); } diff --git a/apps/console/src/pages/system/RoleManagementPage.tsx b/apps/console/src/pages/system/RoleManagementPage.tsx index 0dcb3d9a3..dcceeec3c 100644 --- a/apps/console/src/pages/system/RoleManagementPage.tsx +++ b/apps/console/src/pages/system/RoleManagementPage.tsx @@ -1,144 +1,26 @@ /** * Role Management Page * - * Displays roles with permission assignment matrix. - * Fetches data via dataSource.find('sys_role') and supports - * create / delete operations. + * Metadata-driven role management powered by ObjectView. + * The sys_role object definition in systemObjects.ts drives + * the table columns, search, sort, and CRUD operations. */ -import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@object-ui/auth'; -import { Button, Card, CardContent, Badge } from '@object-ui/components'; -import { Plus, Shield, Loader2, Trash2 } from 'lucide-react'; -import { toast } from 'sonner'; -import { useAdapter } from '../../context/AdapterProvider'; -import { systemObjects } from './systemObjects'; - -const roleObject = systemObjects.find((o) => o.name === 'sys_role')!; -const columns = roleObject.views[0].columns; +import { Shield } from 'lucide-react'; +import { SystemObjectViewPage } from './SystemObjectViewPage'; export function RoleManagementPage() { const { user: currentUser } = useAuth(); const isAdmin = currentUser?.role === 'admin'; - const dataSource = useAdapter(); - - const [records, setRecords] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchData = useCallback(async () => { - if (!dataSource) return; - setLoading(true); - try { - const result = await dataSource.find('sys_role'); - setRecords(result.data || []); - } catch { - toast.error('Failed to load roles'); - } finally { - setLoading(false); - } - }, [dataSource]); - - useEffect(() => { fetchData(); }, [fetchData]); - - const handleCreate = useCallback(async () => { - if (!dataSource) return; - try { - await dataSource.create('sys_role', { name: 'New Role', description: '', isSystem: false }); - toast.success('Role created'); - fetchData(); - } catch { - toast.error('Failed to create role'); - } - }, [dataSource, fetchData]); - - const handleDelete = useCallback(async (id: string) => { - if (!dataSource) return; - try { - await dataSource.delete('sys_role', id); - toast.success('Role deleted'); - fetchData(); - } catch { - toast.error('Failed to delete role'); - } - }, [dataSource, fetchData]); return ( -
-
-
-
- -
-
-

Role Management

-

Define roles and assign permissions

-
-
- {isAdmin && ( - - )} -
- - - -
- - - - {columns.map((col) => { - const field = roleObject.fields.find((f) => f.name === col); - return ( - - ); - })} - {isAdmin && } - - - - {loading ? ( - - - - ) : records.length === 0 ? ( - - - - ) : ( - records.map((record) => ( - - {columns.map((col) => ( - - ))} - {isAdmin && ( - - )} - - )) - )} - -
- {field?.label ?? col} - Actions
- -
-
- -

No roles found.

- plugin-grid powered -
-
- {String(record[col] ?? '')} - - -
-
-
-
-
+ ); } diff --git a/apps/console/src/pages/system/SystemObjectViewPage.tsx b/apps/console/src/pages/system/SystemObjectViewPage.tsx new file mode 100644 index 000000000..7f96fbe66 --- /dev/null +++ b/apps/console/src/pages/system/SystemObjectViewPage.tsx @@ -0,0 +1,91 @@ +/** + * SystemObjectViewPage + * + * Reusable wrapper that renders a system management page using the + * metadata-driven ObjectView from @object-ui/plugin-view. + * + * Each system object's metadata (from systemObjects.ts) drives the table + * columns, search, filtering, and CRUD operations automatically — + * replacing the previous hand-written table implementations. + */ + +import type { ComponentType } from 'react'; +import { ObjectView } from '@object-ui/plugin-view'; +import type { ObjectViewSchema } from '@object-ui/types'; +import { useAdapter } from '../../context/AdapterProvider'; +import { systemObjects } from './systemObjects'; + +interface SystemObjectViewPageProps { + /** System object name (e.g. 'sys_user', 'sys_org') */ + objectName: string; + /** Page heading */ + title: string; + /** Page subtitle */ + description: string; + /** Lucide icon component */ + icon: ComponentType<{ className?: string }>; + /** When true, disables all mutation operations (create/update/delete) */ + readOnly?: boolean; + /** Whether the current user has admin privileges (enables CRUD) */ + isAdmin?: boolean; +} + +export function SystemObjectViewPage({ + objectName, + title, + description, + icon: Icon, + readOnly = false, + isAdmin = false, +}: SystemObjectViewPageProps) { + const dataSource = useAdapter(); + const objDef = systemObjects.find((o) => o.name === objectName); + + if (!objDef) { + return ( +
+

+ System object "{objectName}" not found in metadata definitions. +

+
+ ); + } + + const viewDef = objDef.views?.[0]; + const canMutate = !readOnly && isAdmin; + + const schema: ObjectViewSchema = { + type: 'object-view', + objectName, + showSearch: true, + showFilters: true, + showSort: true, + showCreate: canMutate, + table: { + columns: viewDef?.columns, + }, + operations: { + create: canMutate, + update: canMutate, + delete: canMutate, + }, + }; + + return ( +
+
+
+ +
+
+

{title}

+

{description}

+
+
+ + {dataSource && ( + + )} +
+ ); +} diff --git a/apps/console/src/pages/system/UserManagementPage.tsx b/apps/console/src/pages/system/UserManagementPage.tsx index f69ca8083..c21b7463d 100644 --- a/apps/console/src/pages/system/UserManagementPage.tsx +++ b/apps/console/src/pages/system/UserManagementPage.tsx @@ -1,144 +1,26 @@ /** * User Management Page * - * Displays a list of system users with CRUD capabilities. - * Fetches data via dataSource.find('sys_user') and supports - * create / delete operations. + * Metadata-driven user management powered by ObjectView. + * The sys_user object definition in systemObjects.ts drives + * the table columns, search, sort, and CRUD operations. */ -import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@object-ui/auth'; -import { Button, Card, CardContent, Badge } from '@object-ui/components'; -import { Plus, Users, Loader2, Trash2 } from 'lucide-react'; -import { toast } from 'sonner'; -import { useAdapter } from '../../context/AdapterProvider'; -import { systemObjects } from './systemObjects'; - -const userObject = systemObjects.find((o) => o.name === 'sys_user')!; -const columns = userObject.views[0].columns; +import { Users } from 'lucide-react'; +import { SystemObjectViewPage } from './SystemObjectViewPage'; export function UserManagementPage() { const { user: currentUser } = useAuth(); const isAdmin = currentUser?.role === 'admin'; - const dataSource = useAdapter(); - - const [records, setRecords] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchData = useCallback(async () => { - if (!dataSource) return; - setLoading(true); - try { - const result = await dataSource.find('sys_user'); - setRecords(result.data || []); - } catch { - toast.error('Failed to load users'); - } finally { - setLoading(false); - } - }, [dataSource]); - - useEffect(() => { fetchData(); }, [fetchData]); - - const handleCreate = useCallback(async () => { - if (!dataSource) return; - try { - await dataSource.create('sys_user', { name: 'New User', email: '', role: 'member', status: 'active' }); - toast.success('User created'); - fetchData(); - } catch { - toast.error('Failed to create user'); - } - }, [dataSource, fetchData]); - - const handleDelete = useCallback(async (id: string) => { - if (!dataSource) return; - try { - await dataSource.delete('sys_user', id); - toast.success('User deleted'); - fetchData(); - } catch { - toast.error('Failed to delete user'); - } - }, [dataSource, fetchData]); return ( -
-
-
-
- -
-
-

User Management

-

Manage system users and their roles

-
-
- {isAdmin && ( - - )} -
- - - -
- - - - {columns.map((col) => { - const field = userObject.fields.find((f) => f.name === col); - return ( - - ); - })} - {isAdmin && } - - - - {loading ? ( - - - - ) : records.length === 0 ? ( - - - - ) : ( - records.map((record) => ( - - {columns.map((col) => ( - - ))} - {isAdmin && ( - - )} - - )) - )} - -
- {field?.label ?? col} - Actions
- -
-
- -

No users found.

- plugin-grid powered -
-
- {String(record[col] ?? '')} - - -
-
-
-
-
+ ); }