Skip to content

Commit f7733b1

Browse files
authored
Merge pull request #1139 from objectstack-ai/copilot/refactor-system-settings-page
2 parents 70e3316 + ee97b35 commit f7733b1

File tree

9 files changed

+258
-751
lines changed

9 files changed

+258
-751
lines changed

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **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.
13+
1014
### Fixed
1115

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

ROADMAP.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,17 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
757757
- [x] Search/filter permissions
758758
- [x] Admin-only create/delete controls
759759

760+
**ObjectView-Driven System Pages (P1.12.2):**
761+
- [x] Shared `SystemObjectViewPage` component using `ObjectView` from `@object-ui/plugin-view`
762+
- [x] User Management (`/system/users`) driven by `sys_user` metadata via ObjectView
763+
- [x] Organization Management (`/system/organizations`) driven by `sys_org` metadata via ObjectView
764+
- [x] Role Management (`/system/roles`) driven by `sys_role` metadata via ObjectView
765+
- [x] Permission Management (`/system/permissions`) driven by `sys_permission` metadata via ObjectView
766+
- [x] Audit Log (`/system/audit-log`) driven by `sys_audit_log` metadata via ObjectView (read-only)
767+
- [x] Admin-only CRUD operations controlled via ObjectView `operations` config
768+
- [x] Automatic search, sort, filter, pagination from ObjectView capabilities
769+
- [x] 22 system page tests passing
770+
760771
**Sidebar & Navigation Updates:**
761772
- [x] Settings button → `/system/` hub (was `/system/profile`)
762773
- [x] App switcher "Manage All Apps" link → `/system/apps`
Lines changed: 91 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,48 @@
11
/**
22
* System Admin Pages Integration Tests
33
*
4-
* Tests that system pages (User, Org, Role, AuditLog) fetch data
5-
* via useAdapter() and render records from the API.
4+
* Tests that system pages render the correct page header and delegate
5+
* data rendering to the ObjectView component from @object-ui/plugin-view,
6+
* configured with the appropriate object metadata from systemObjects.ts.
67
*/
78

89
import { describe, it, expect, vi, beforeEach } from 'vitest';
910
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
1011
import '@testing-library/jest-dom';
1112
import { MemoryRouter } from 'react-router-dom';
1213

14+
// --- Capture ObjectView props for assertion ---
15+
let lastObjectViewProps: any = null;
16+
17+
vi.mock('@object-ui/plugin-view', () => ({
18+
ObjectView: (props: any) => {
19+
lastObjectViewProps = props;
20+
return (
21+
<div
22+
data-testid="plugin-object-view"
23+
data-objectname={props.schema?.objectName}
24+
data-operations={JSON.stringify(props.schema?.operations)}
25+
/>
26+
);
27+
},
28+
}));
29+
1330
// --- Shared mock adapter ---
1431
const mockFind = vi.fn().mockResolvedValue({ data: [], total: 0 });
1532
const mockCreate = vi.fn().mockResolvedValue({ id: 'new-1' });
1633
const mockDelete = vi.fn().mockResolvedValue({});
1734

35+
const mockAdapter = {
36+
find: mockFind,
37+
create: mockCreate,
38+
delete: mockDelete,
39+
update: vi.fn(),
40+
findOne: vi.fn(),
41+
getObjectSchema: vi.fn().mockResolvedValue({ name: 'test', fields: {} }),
42+
};
43+
1844
vi.mock('../context/AdapterProvider', () => ({
19-
useAdapter: () => ({
20-
find: mockFind,
21-
create: mockCreate,
22-
delete: mockDelete,
23-
update: vi.fn(),
24-
findOne: vi.fn(),
25-
}),
45+
useAdapter: () => mockAdapter,
2646
}));
2747

2848
vi.mock('@object-ui/auth', () => ({
@@ -75,110 +95,88 @@ function wrap(ui: React.ReactElement) {
7595

7696
beforeEach(() => {
7797
vi.clearAllMocks();
98+
lastObjectViewProps = null;
7899
});
79100

80101
describe('UserManagementPage', () => {
81-
it('should call dataSource.find("sys_user") on mount', async () => {
82-
mockFind.mockResolvedValueOnce({
83-
data: [{ id: '1', name: 'Alice', email: 'alice@test.com', role: 'admin', status: 'active', lastLoginAt: '' }],
84-
});
102+
it('should render ObjectView with sys_user object and page header', () => {
85103
wrap(<UserManagementPage />);
86-
await waitFor(() => {
87-
expect(mockFind).toHaveBeenCalledWith('sys_user');
88-
});
89-
await waitFor(() => {
90-
expect(screen.getByText('Alice')).toBeInTheDocument();
91-
});
104+
expect(screen.getByText('User Management')).toBeInTheDocument();
105+
expect(screen.getByText('Manage system users and their roles')).toBeInTheDocument();
106+
expect(screen.getByTestId('plugin-object-view')).toBeInTheDocument();
107+
expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_user');
92108
});
93109

94-
it('should show empty state when no users', async () => {
95-
mockFind.mockResolvedValueOnce({ data: [] });
110+
it('should pass the adapter as dataSource to ObjectView', () => {
96111
wrap(<UserManagementPage />);
97-
await waitFor(() => {
98-
expect(screen.getByText('No users found.')).toBeInTheDocument();
99-
});
112+
expect(lastObjectViewProps.dataSource).toBe(mockAdapter);
100113
});
101114

102-
it('should call create when Add User is clicked', async () => {
103-
mockFind.mockResolvedValue({ data: [] });
104-
mockCreate.mockResolvedValueOnce({ id: 'new-user' });
115+
it('should enable CRUD operations for admin users', () => {
105116
wrap(<UserManagementPage />);
106-
await waitFor(() => {
107-
expect(screen.getByText('No users found.')).toBeInTheDocument();
108-
});
109-
fireEvent.click(screen.getByText('Add User'));
110-
await waitFor(() => {
111-
expect(mockCreate).toHaveBeenCalledWith('sys_user', expect.objectContaining({ name: 'New User' }));
112-
});
117+
const ops = lastObjectViewProps.schema.operations;
118+
expect(ops).toEqual({ create: true, update: true, delete: true });
119+
});
120+
121+
it('should configure table columns from systemObjects metadata', () => {
122+
wrap(<UserManagementPage />);
123+
expect(lastObjectViewProps.schema.table.columns).toEqual(
124+
['name', 'email', 'role', 'status', 'lastLoginAt']
125+
);
113126
});
114127
});
115128

116129
describe('OrgManagementPage', () => {
117-
it('should call dataSource.find("sys_org") on mount', async () => {
118-
mockFind.mockResolvedValueOnce({
119-
data: [{ id: '1', name: 'Acme', slug: 'acme', plan: 'pro', status: 'active', memberCount: 5 }],
120-
});
130+
it('should render ObjectView with sys_org object and page header', () => {
121131
wrap(<OrgManagementPage />);
122-
await waitFor(() => {
123-
expect(mockFind).toHaveBeenCalledWith('sys_org');
124-
});
125-
await waitFor(() => {
126-
expect(screen.getByText('Acme')).toBeInTheDocument();
127-
});
132+
expect(screen.getByText('Organization Management')).toBeInTheDocument();
133+
expect(screen.getByText('Manage organizations and their members')).toBeInTheDocument();
134+
expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_org');
128135
});
129136

130-
it('should show empty state when no organizations', async () => {
131-
mockFind.mockResolvedValueOnce({ data: [] });
137+
it('should configure table columns from systemObjects metadata', () => {
132138
wrap(<OrgManagementPage />);
133-
await waitFor(() => {
134-
expect(screen.getByText('No organizations found.')).toBeInTheDocument();
135-
});
139+
expect(lastObjectViewProps.schema.table.columns).toEqual(
140+
['name', 'slug', 'plan', 'status', 'memberCount']
141+
);
136142
});
137143
});
138144

139145
describe('RoleManagementPage', () => {
140-
it('should call dataSource.find("sys_role") on mount', async () => {
141-
mockFind.mockResolvedValueOnce({
142-
data: [{ id: '1', name: 'Admin', description: 'Full access', isSystem: true, userCount: 3 }],
143-
});
146+
it('should render ObjectView with sys_role object and page header', () => {
144147
wrap(<RoleManagementPage />);
145-
await waitFor(() => {
146-
expect(mockFind).toHaveBeenCalledWith('sys_role');
147-
});
148-
await waitFor(() => {
149-
expect(screen.getByText('Admin')).toBeInTheDocument();
150-
});
148+
expect(screen.getByText('Role Management')).toBeInTheDocument();
149+
expect(screen.getByText('Define roles and assign permissions')).toBeInTheDocument();
150+
expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_role');
151151
});
152152

153-
it('should show empty state when no roles', async () => {
154-
mockFind.mockResolvedValueOnce({ data: [] });
153+
it('should configure table columns from systemObjects metadata', () => {
155154
wrap(<RoleManagementPage />);
156-
await waitFor(() => {
157-
expect(screen.getByText('No roles found.')).toBeInTheDocument();
158-
});
155+
expect(lastObjectViewProps.schema.table.columns).toEqual(
156+
['name', 'description', 'isSystem', 'userCount']
157+
);
159158
});
160159
});
161160

162161
describe('AuditLogPage', () => {
163-
it('should call dataSource.find("sys_audit_log") on mount', async () => {
164-
mockFind.mockResolvedValueOnce({
165-
data: [{ id: '1', action: 'create', resource: 'user', userName: 'Admin', ipAddress: '127.0.0.1', createdAt: '2026-01-01' }],
166-
});
162+
it('should render ObjectView with sys_audit_log object and page header', () => {
167163
wrap(<AuditLogPage />);
168-
await waitFor(() => {
169-
expect(mockFind).toHaveBeenCalledWith('sys_audit_log', expect.objectContaining({ $orderby: { createdAt: 'desc' } }));
170-
});
171-
await waitFor(() => {
172-
expect(screen.getByText('create')).toBeInTheDocument();
173-
});
164+
expect(screen.getByText('Audit Log')).toBeInTheDocument();
165+
expect(screen.getByText('View system activity and user actions')).toBeInTheDocument();
166+
expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_audit_log');
174167
});
175168

176-
it('should show empty state when no logs', async () => {
177-
mockFind.mockResolvedValueOnce({ data: [] });
169+
it('should disable all mutation operations (read-only)', () => {
178170
wrap(<AuditLogPage />);
179-
await waitFor(() => {
180-
expect(screen.getByText('No audit logs found.')).toBeInTheDocument();
181-
});
171+
const ops = lastObjectViewProps.schema.operations;
172+
expect(ops).toEqual({ create: false, update: false, delete: false });
173+
});
174+
175+
it('should configure table columns from systemObjects metadata', () => {
176+
wrap(<AuditLogPage />);
177+
expect(lastObjectViewProps.schema.table.columns).toEqual(
178+
['action', 'resource', 'userName', 'ipAddress', 'createdAt']
179+
);
182180
});
183181
});
184182

@@ -250,56 +248,29 @@ describe('AppManagementPage', () => {
250248
});
251249

252250
describe('PermissionManagementPage', () => {
253-
it('should call dataSource.find("sys_permission") on mount', async () => {
254-
mockFind.mockResolvedValueOnce({
255-
data: [{ id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: 'Full user access' }],
256-
});
251+
it('should render ObjectView with sys_permission object and page header', () => {
257252
wrap(<PermissionManagementPage />);
258-
await waitFor(() => {
259-
expect(mockFind).toHaveBeenCalledWith('sys_permission');
260-
});
261-
await waitFor(() => {
262-
expect(screen.getByText('manage_users')).toBeInTheDocument();
263-
});
253+
expect(screen.getByText('Permissions')).toBeInTheDocument();
254+
expect(screen.getByText('Manage permission rules and assignments')).toBeInTheDocument();
255+
expect(screen.getByTestId('plugin-object-view').dataset.objectname).toBe('sys_permission');
264256
});
265257

266-
it('should show empty state when no permissions', async () => {
267-
mockFind.mockResolvedValueOnce({ data: [] });
258+
it('should enable CRUD operations for admin users', () => {
268259
wrap(<PermissionManagementPage />);
269-
await waitFor(() => {
270-
expect(screen.getByText('No permissions found.')).toBeInTheDocument();
271-
});
260+
const ops = lastObjectViewProps.schema.operations;
261+
expect(ops).toEqual({ create: true, update: true, delete: true });
272262
});
273263

274-
it('should call create when Add Permission is clicked', async () => {
275-
mockFind.mockResolvedValue({ data: [] });
276-
mockCreate.mockResolvedValueOnce({ id: 'new-perm' });
264+
it('should configure table columns from systemObjects metadata', () => {
277265
wrap(<PermissionManagementPage />);
278-
await waitFor(() => {
279-
expect(screen.getByText('No permissions found.')).toBeInTheDocument();
280-
});
281-
fireEvent.click(screen.getByText('Add Permission'));
282-
await waitFor(() => {
283-
expect(mockCreate).toHaveBeenCalledWith('sys_permission', expect.objectContaining({ name: 'New Permission' }));
284-
});
266+
expect(lastObjectViewProps.schema.table.columns).toEqual(
267+
['name', 'resource', 'action', 'description']
268+
);
285269
});
286270

287-
it('should filter permissions by search query', async () => {
288-
mockFind.mockResolvedValue({
289-
data: [
290-
{ id: '1', name: 'manage_users', resource: 'user', action: 'manage', description: '' },
291-
{ id: '2', name: 'read_reports', resource: 'report', action: 'read', description: '' },
292-
],
293-
});
271+
it('should enable search and filters', () => {
294272
wrap(<PermissionManagementPage />);
295-
await waitFor(() => {
296-
expect(screen.getByText('manage_users')).toBeInTheDocument();
297-
expect(screen.getByText('read_reports')).toBeInTheDocument();
298-
});
299-
fireEvent.change(screen.getByTestId('permission-search-input'), { target: { value: 'report' } });
300-
await waitFor(() => {
301-
expect(screen.queryByText('manage_users')).not.toBeInTheDocument();
302-
expect(screen.getByText('read_reports')).toBeInTheDocument();
303-
});
273+
expect(lastObjectViewProps.schema.showSearch).toBe(true);
274+
expect(lastObjectViewProps.schema.showFilters).toBe(true);
304275
});
305276
});

0 commit comments

Comments
 (0)