@nocobase/app-template-default 1.0.0-beta.75 → 1.0.0-beta.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.agents/rules/build-and-deploy.md +4 -0
  2. package/.agents/rules/components.md +3 -1
  3. package/README.MD +2 -2
  4. package/client/components/data-table/data-table.tsx +2 -0
  5. package/client/components/data-table/pagination.tsx +6 -6
  6. package/client/components/default-access-editor.tsx +79 -52
  7. package/client/components/flow-diagram.tsx +645 -83
  8. package/client/components/flow-layout.ts +117 -7
  9. package/client/components/lifecycle-diagram.tsx +175 -37
  10. package/client/components/long-text.tsx +200 -0
  11. package/client/components/permission-editor.tsx +534 -341
  12. package/client/components/record-link.tsx +20 -0
  13. package/client/components/required-role-setup.tsx +93 -0
  14. package/client/components/role-list.tsx +21 -5
  15. package/client/components/setup-readiness-state.ts +135 -0
  16. package/client/components/setup-readiness.tsx +91 -0
  17. package/client/components/ui/combobox.tsx +300 -0
  18. package/client/extensions/nocobase-workflow-management/README.md +3 -1
  19. package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
  20. package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
  21. package/client/hooks/use-table-url-state.ts +93 -0
  22. package/client/lib/required-role-setup.ts +98 -0
  23. package/client/lib/return-to.ts +19 -0
  24. package/client/locales/system/en-US.ts +10 -0
  25. package/client/locales/system/settings-en-US.ts +51 -4
  26. package/client/locales/system/settings-zh-CN.ts +46 -5
  27. package/client/locales/system/zh-CN.ts +10 -0
  28. package/client/pages/settings/default-access-form.tsx +14 -2
  29. package/client/pages/settings/default-access-model.ts +6 -2
  30. package/client/pages/settings/default-access.tsx +40 -8
  31. package/client/pages/settings/page-permissions.ts +166 -1
  32. package/client/pages/settings/permission-filters.tsx +9 -2
  33. package/client/pages/settings/permission-workspace.ts +330 -0
  34. package/client/pages/settings/permissions-model.ts +62 -53
  35. package/client/pages/settings/role-form.tsx +31 -95
  36. package/client/pages/settings/roles.tsx +44 -6
  37. package/client/pages/settings/shared.tsx +15 -13
  38. package/client/pages/settings/user-detail.tsx +15 -259
  39. package/client/pages/settings/user-form.tsx +299 -147
  40. package/client/pages/settings/users.tsx +211 -123
  41. package/config.example.yml +6 -5
  42. package/package.json +17 -17
  43. package/server/config/app.ts +1 -1
  44. package/server/config/session.ts +7 -5
  45. package/tests/components/default-access-editor.test.tsx +130 -0
  46. package/tests/components/default-access.test.tsx +141 -2
  47. package/tests/components/permission-editor.test.tsx +613 -572
  48. package/tests/components/permission-filter-binding.test.tsx +20 -0
  49. package/tests/components/settings-continuity.test.tsx +426 -0
  50. package/tests/components/settings-feedback.test.tsx +304 -0
  51. package/tests/components/settings.test.tsx +34 -9
  52. package/tests/components/setup-readiness.test.tsx +370 -0
  53. package/tests/components/table-url-state.test.tsx +95 -0
  54. package/tests/components/user-drawer.test.tsx +526 -0
  55. package/tests/components/user-permissions.test.tsx +47 -13
  56. package/tests/logic/config.test.ts +3 -1
  57. package/tests/logic/page-permission-navigation.test.ts +394 -0
  58. package/tests/logic/permission-workspace.test.ts +570 -0
  59. package/tests/logic/return-to.test.ts +19 -0
  60. package/tests/logic/session-isolation.test.ts +250 -0
  61. package/tsconfig.json +1 -0
@@ -0,0 +1,394 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ defineAppRoutes,
4
+ resolveAppClientContributions,
5
+ } from '@nocobase/app-client/plugins';
6
+ import type { LocalizedText } from '@nocobase/app-plugin-authorization/client';
7
+ import type {
8
+ PermissionEditorNavigationNode,
9
+ PermissionEditorSection,
10
+ } from '../../client/components/permission-editor.js';
11
+ import {
12
+ pagePermissionNavigation,
13
+ pagePermissionResources,
14
+ permissionNavigationSections,
15
+ } from '../../client/pages/settings/page-permissions.js';
16
+ import enUS from '../../client/locales/en-US.js';
17
+ import zhCN from '../../client/locales/zh-CN.js';
18
+
19
+ const componentLoader = async () => ({ default: () => null });
20
+ const text = (value: LocalizedText): string =>
21
+ typeof value === 'string' ? value : value.key;
22
+ const resource = (id: string) => ({
23
+ resource: { type: 'page' as const, id },
24
+ action: 'access',
25
+ });
26
+ const flatten = (
27
+ nodes: readonly PermissionEditorNavigationNode[],
28
+ ): PermissionEditorNavigationNode[] =>
29
+ nodes.flatMap((node) => [node, ...flatten(node.children ?? [])]);
30
+ const fixture = () =>
31
+ resolveAppClientContributions([
32
+ {
33
+ packageName: 'private-app',
34
+ source: 'application',
35
+ routes: defineAppRoutes([
36
+ {
37
+ name: 'basic',
38
+ navigation: { title: 'Basic data' },
39
+ children: [
40
+ {
41
+ name: 'materials',
42
+ path: '/materials',
43
+ navigation: { title: 'Materials' },
44
+ authz: resource('legacy-materials'),
45
+ componentLoader,
46
+ },
47
+ {
48
+ name: 'locations',
49
+ navigation: { title: 'Locations' },
50
+ children: [
51
+ {
52
+ name: 'warehouses',
53
+ path: '/warehouses',
54
+ navigation: { title: 'Warehouses' },
55
+ authz: resource('legacy-warehouses'),
56
+ componentLoader,
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ {
63
+ name: 'loans',
64
+ path: '/loans',
65
+ navigation: { title: 'Loans' },
66
+ authz: resource('legacy-loans'),
67
+ componentLoader,
68
+ children: [
69
+ { name: 'loanDetail', path: ':id', componentLoader },
70
+ {
71
+ name: 'reviews',
72
+ path: 'reviews',
73
+ navigation: { title: 'Reviews' },
74
+ authz: resource('legacy-reviews'),
75
+ componentLoader,
76
+ },
77
+ {
78
+ name: 'audit',
79
+ path: 'audit',
80
+ authz: resource('legacy-audit'),
81
+ componentLoader,
82
+ },
83
+ ],
84
+ },
85
+ {
86
+ name: 'alias',
87
+ path: '/loan-alias',
88
+ navigation: { title: 'Loan alias' },
89
+ authz: resource('legacy-loans'),
90
+ componentLoader,
91
+ },
92
+ {
93
+ name: 'settings',
94
+ path: '/settings',
95
+ navigation: { title: 'Settings' },
96
+ authz: {
97
+ resource: { type: 'settings', id: 'rules' },
98
+ action: 'read',
99
+ },
100
+ componentLoader,
101
+ },
102
+ {
103
+ name: 'landing',
104
+ path: '/',
105
+ navigation: { title: 'Landing' },
106
+ authz: 'skip',
107
+ componentLoader,
108
+ },
109
+ ]),
110
+ },
111
+ ]);
112
+
113
+ describe('page permission navigation projection', () => {
114
+ it('supplies named disclosure and non-menu labels in both supported languages', () => {
115
+ for (const locale of [enUS, zhCN]) {
116
+ const settings = Reflect.get(locale, 'settings') as Record<
117
+ string,
118
+ unknown
119
+ >;
120
+ for (const key of [
121
+ 'expandPermissionGroup',
122
+ 'collapsePermissionGroup',
123
+ 'pagesOutsideNavigation',
124
+ ]) {
125
+ const value = settings[key];
126
+ expect(typeof value).toBe('string');
127
+ expect(String(value).trim()).not.toBe('');
128
+ expect(value).not.toBe(key);
129
+ if (key !== 'pagesOutsideNavigation')
130
+ expect(value).toContain('{{name}}');
131
+ }
132
+ }
133
+ });
134
+ it('preserves real groups, deeper menus and independent parent/child identities', () => {
135
+ const { routes } = fixture();
136
+ const result = pagePermissionNavigation(
137
+ routes,
138
+ { audit: 'Audit details' },
139
+ (path) => `Unnamed (${path})`,
140
+ text,
141
+ );
142
+ expect(result.map((node) => node.label)).toEqual([
143
+ 'Basic data',
144
+ 'Loans',
145
+ 'settings.pagesOutsideNavigation',
146
+ ]);
147
+ expect(result[0]?.rowId).toBeUndefined();
148
+ expect(result[0]?.children?.map((node) => node.label)).toEqual([
149
+ 'Materials',
150
+ 'Locations',
151
+ ]);
152
+ expect(result[0]?.children?.[1]?.children?.[0]?.label).toBe('Warehouses');
153
+ expect(result[1]?.rowId).toBe('["page","legacy-loans","access"]');
154
+ expect(result[1]?.children?.[0]?.rowId).toBe(
155
+ '["page","legacy-reviews","access"]',
156
+ );
157
+ expect(
158
+ flatten(result)
159
+ .filter((node) => node.rowId)
160
+ .map((node) => node.rowId),
161
+ ).toEqual([
162
+ '["page","legacy-materials","access"]',
163
+ '["page","legacy-warehouses","access"]',
164
+ '["page","legacy-loans","access"]',
165
+ '["page","legacy-reviews","access"]',
166
+ '["page","legacy-audit","access"]',
167
+ ]);
168
+ });
169
+
170
+ it('keeps actual non-menu independent pages and deduplicates inherited and aliased grants', () => {
171
+ const { routes } = fixture();
172
+ const before = JSON.stringify(routes);
173
+ const pages = pagePermissionResources(
174
+ routes,
175
+ { audit: 'Audit details' },
176
+ (path) => `Unnamed (${path})`,
177
+ );
178
+ const result = pagePermissionNavigation(
179
+ routes,
180
+ { audit: 'Audit details' },
181
+ (path) => `Unnamed (${path})`,
182
+ text,
183
+ );
184
+ expect(result.at(-1)?.children).toEqual([
185
+ {
186
+ id: 'page:legacy-audit',
187
+ label: 'Audit details',
188
+ rowId: '["page","legacy-audit","access"]',
189
+ },
190
+ ]);
191
+ expect(flatten(result).filter((node) => node.rowId)).toHaveLength(
192
+ pages.length,
193
+ );
194
+ expect(flatten(result).map((node) => node.label)).not.toContain(
195
+ 'Loan alias',
196
+ );
197
+ expect(flatten(result).map((node) => node.label)).not.toContain('Landing');
198
+ expect(flatten(result).map((node) => node.label)).not.toContain('Settings');
199
+ expect(JSON.stringify(routes)).toBe(before);
200
+ expect(
201
+ pagePermissionNavigation(
202
+ routes,
203
+ { audit: 'Audit details' },
204
+ (path) => `Unnamed (${path})`,
205
+ text,
206
+ ),
207
+ ).toEqual(result);
208
+ });
209
+
210
+ it('renders single-level menus at a single level without inventing groups', () => {
211
+ const { routes } = resolveAppClientContributions([
212
+ {
213
+ packageName: 'flat-app',
214
+ source: 'application',
215
+ routes: defineAppRoutes([
216
+ {
217
+ name: 'one',
218
+ path: '/one',
219
+ navigation: { title: 'One' },
220
+ authz: resource('one'),
221
+ componentLoader,
222
+ },
223
+ {
224
+ name: 'two',
225
+ path: '/two',
226
+ navigation: { title: 'Two' },
227
+ authz: resource('two'),
228
+ componentLoader,
229
+ },
230
+ ]),
231
+ },
232
+ ]);
233
+ const result = pagePermissionNavigation(routes, {}, () => 'Unnamed', text);
234
+ expect(result.map((node) => node.label)).toEqual(['One', 'Two']);
235
+ expect(result.every((node) => !node.children?.length)).toBe(true);
236
+ expect(result.every((node) => node.rowId)).toBe(true);
237
+ });
238
+ });
239
+
240
+ it('omits menus governed by settings or account permissions instead of mirroring them as page controls or links', () => {
241
+ const { routes } = resolveAppClientContributions([
242
+ {
243
+ packageName: 'app',
244
+ source: 'application',
245
+ routes: defineAppRoutes([
246
+ {
247
+ name: 'settings',
248
+ path: '/settings',
249
+ navigation: { title: 'System settings' },
250
+ children: [
251
+ {
252
+ name: 'users',
253
+ path: 'users',
254
+ navigation: { title: 'User management' },
255
+ authz: { resource: { type: 'user', id: '*' }, action: 'read' },
256
+ componentLoader,
257
+ },
258
+ {
259
+ name: 'roles',
260
+ path: 'roles',
261
+ navigation: { title: 'Permission management' },
262
+ authz: {
263
+ resource: {
264
+ type: 'settings',
265
+ id: 'authorization.permission-sets',
266
+ },
267
+ action: 'read',
268
+ },
269
+ componentLoader,
270
+ },
271
+ {
272
+ name: 'logs',
273
+ path: 'logs',
274
+ navigation: { title: 'Notification logs' },
275
+ authz: resource('logs'),
276
+ componentLoader,
277
+ },
278
+ ],
279
+ },
280
+ ]),
281
+ },
282
+ ]);
283
+ const result = pagePermissionNavigation(routes, {}, (path) => path, text);
284
+ expect(result).toEqual([]);
285
+ const withoutPage = routes.map((route) => ({
286
+ ...route,
287
+ children: route.children?.filter(({ name }) => name !== 'logs'),
288
+ }));
289
+ expect(
290
+ pagePermissionNavigation(withoutPage, {}, (path) => path, text),
291
+ ).toEqual([]);
292
+ });
293
+
294
+ it.each(['settings', 'systemManagement'])(
295
+ 'moves all %s controls into configuration without changing or duplicating grants',
296
+ (rootName) => {
297
+ const { routes } = resolveAppClientContributions([
298
+ {
299
+ packageName: 'app',
300
+ source: 'application',
301
+ routes: defineAppRoutes([
302
+ {
303
+ name: 'orders',
304
+ path: '/orders',
305
+ navigation: { title: 'Orders' },
306
+ authz: resource('orders'),
307
+ componentLoader,
308
+ },
309
+ {
310
+ name: rootName,
311
+ path: '/settings',
312
+ navigation: { title: 'Settings' },
313
+ children: [
314
+ {
315
+ name: 'users',
316
+ path: 'users',
317
+ navigation: { title: 'Users' },
318
+ authz: { resource: { type: 'user', id: '*' }, action: 'read' },
319
+ componentLoader,
320
+ },
321
+ {
322
+ name: 'logs',
323
+ path: 'logs',
324
+ navigation: { title: 'Logs' },
325
+ authz: resource('logs'),
326
+ componentLoader,
327
+ },
328
+ ],
329
+ },
330
+ ]),
331
+ },
332
+ ]);
333
+ const user = {
334
+ id: '["user","*","read"]',
335
+ label: 'View users',
336
+ enabled: true,
337
+ };
338
+ const accountAction = {
339
+ id: '["user","*","disable"]',
340
+ label: 'Disable account',
341
+ enabled: false,
342
+ };
343
+ const order = {
344
+ id: '["page","orders","access"]',
345
+ label: 'Orders',
346
+ enabled: true,
347
+ };
348
+ const logs = {
349
+ id: '["page","logs","access"]',
350
+ label: 'Logs',
351
+ enabled: false,
352
+ };
353
+ const sections: PermissionEditorSection[] = [
354
+ {
355
+ id: 'users',
356
+ title: 'Users and accounts',
357
+ groups: [{ id: 'users', rows: [user, accountAction] }],
358
+ },
359
+ {
360
+ id: 'pages',
361
+ title: 'Page access',
362
+ groups: [
363
+ { id: 'orders', accessOnly: true, rows: [order] },
364
+ { id: 'logs', accessOnly: true, rows: [logs] },
365
+ ],
366
+ },
367
+ ];
368
+ const before = JSON.stringify(sections);
369
+ const result = permissionNavigationSections(
370
+ sections,
371
+ routes,
372
+ {},
373
+ (path) => path,
374
+ text,
375
+ );
376
+ expect(result.map(({ id }) => id)).toEqual(['pages', 'configuration']);
377
+ expect(result[0]?.groups.flatMap(({ rows }) => rows)).toEqual([order]);
378
+ expect(
379
+ flatten(result[0]?.navigation ?? []).flatMap(({ rowId }) =>
380
+ rowId ? [rowId] : [],
381
+ ),
382
+ ).toEqual([order.id]);
383
+ expect(result[1]?.groups.flatMap(({ rows }) => rows)).toEqual([
384
+ user,
385
+ accountAction,
386
+ logs,
387
+ ]);
388
+ expect(result[1]?.groups[0]?.rows[0]).toBe(user);
389
+ expect(JSON.stringify(sections)).toBe(before);
390
+ expect(
391
+ permissionNavigationSections(result, routes, {}, (path) => path, text),
392
+ ).toEqual(result);
393
+ },
394
+ );