@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,130 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expect, it, vi } from 'vitest';
4
+ import { DefaultAccessEditor } from '../../client/components/default-access-editor.js';
5
+
6
+ it('reports one explicit scope choice and does not mutate its controlled fields during browsing', async () => {
7
+ const user = userEvent.setup();
8
+ const onScopeChange = vi.fn();
9
+ const props = {
10
+ ruleKey: 'baseline',
11
+ resource: 'projects',
12
+ resources: [{ value: 'projects', label: 'Projects' }],
13
+ scopes: [
14
+ {
15
+ id: 'view-projects',
16
+ action: 'View',
17
+ scope: 'Projects',
18
+ value: 'own',
19
+ options: [
20
+ { value: 'own', label: 'Own records' },
21
+ { value: 'all', label: 'All records' },
22
+ ],
23
+ },
24
+ ],
25
+ labels: {
26
+ key: 'Key',
27
+ resource: 'Business object',
28
+ chooseResource: 'Choose an object',
29
+ action: 'Operation',
30
+ scope: 'Data scope',
31
+ noScopes: 'Choose an object',
32
+ },
33
+ onKeyChange: vi.fn(),
34
+ onResourceChange: vi.fn(),
35
+ onScopeChange,
36
+ };
37
+ render(<DefaultAccessEditor {...props} existing />);
38
+ expect(screen.getByRole('textbox', { name: 'Key *' })).toBeDisabled();
39
+ expect(props.onKeyChange).not.toHaveBeenCalled();
40
+ await user.click(screen.getByRole('combobox', { name: 'View · Projects' }));
41
+ expect(onScopeChange).not.toHaveBeenCalled();
42
+ await user.click(await screen.findByRole('option', { name: 'All records' }));
43
+ expect(onScopeChange).toHaveBeenCalledExactlyOnceWith('view-projects', 'all');
44
+ });
45
+
46
+ it('groups ranges by operation, keeps independent choices and preserves explicit operation identities', async () => {
47
+ const change = vi.fn();
48
+ const options = [
49
+ { value: '', label: 'No default scope' },
50
+ { value: 'all', label: 'All records' },
51
+ ];
52
+ const props = {
53
+ ruleKey: 'baseline',
54
+ resource: 'library',
55
+ resources: [{ value: 'library', label: 'Library' }],
56
+ scopes: [
57
+ {
58
+ id: 'books',
59
+ actionId: 'browse',
60
+ action: 'Browse holdings',
61
+ scope: 'Book information',
62
+ value: '',
63
+ options,
64
+ },
65
+ {
66
+ id: 'copies',
67
+ actionId: 'browse',
68
+ action: 'Browse holdings',
69
+ scope: 'Physical copies',
70
+ value: '',
71
+ options,
72
+ configuration: <button type='button'>Configure conditions</button>,
73
+ },
74
+ {
75
+ id: 'manage',
76
+ actionId: 'manage',
77
+ action: 'Manage',
78
+ scope: 'Book information',
79
+ value: '',
80
+ options,
81
+ },
82
+ {
83
+ id: 'settings',
84
+ actionId: 'settings',
85
+ action: 'Manage',
86
+ scope: 'Settings',
87
+ value: '',
88
+ options,
89
+ },
90
+ ],
91
+ labels: {
92
+ key: 'Key',
93
+ resource: 'Business object',
94
+ chooseResource: 'Choose',
95
+ action: 'Operation',
96
+ scope: 'Scope',
97
+ noScopes: 'Choose an object',
98
+ },
99
+ onKeyChange: vi.fn(),
100
+ onResourceChange: vi.fn(),
101
+ onScopeChange: change,
102
+ };
103
+ const view = render(<DefaultAccessEditor {...props} />);
104
+ expect(
105
+ screen.getAllByRole('heading', { name: 'Browse holdings' }),
106
+ ).toHaveLength(1);
107
+ expect(screen.getAllByRole('region', { name: 'Manage' })).toHaveLength(2);
108
+ await userEvent.click(
109
+ screen.getByRole('button', { name: 'Configure conditions' }),
110
+ );
111
+ expect(change).not.toHaveBeenCalled();
112
+ await userEvent.click(
113
+ screen.getByRole('combobox', { name: 'Browse holdings · Physical copies' }),
114
+ );
115
+ await userEvent.click(
116
+ await screen.findByRole('option', { name: 'All records' }),
117
+ );
118
+ expect(change).toHaveBeenCalledExactlyOnceWith('copies', 'all');
119
+ expect(
120
+ screen.getByRole('combobox', {
121
+ name: 'Browse holdings · Book information',
122
+ }),
123
+ ).toHaveTextContent('No default scope');
124
+ view.rerender(<DefaultAccessEditor {...props} readOnly />);
125
+ expect(
126
+ screen
127
+ .getAllByRole('combobox')
128
+ .every((item) => item.hasAttribute('disabled')),
129
+ ).toBe(true);
130
+ });
@@ -5,9 +5,9 @@ import {
5
5
  authorizationClientToken,
6
6
  type AuthorizationOptionsResponse,
7
7
  } from '@nocobase/app-plugin-authorization/client';
8
- import { screen, waitFor, within } from '@testing-library/react';
8
+ import { act, screen, waitFor, within } from '@testing-library/react';
9
9
  import userEvent from '@testing-library/user-event';
10
- import { Routes, Route } from 'react-router';
10
+ import { Routes, Route, useLocation, useNavigate } from 'react-router';
11
11
  import { expect, it, vi } from 'vitest';
12
12
  import Page from '../../client/pages/settings/default-access.js';
13
13
  import Form from '../../client/pages/settings/default-access-form.js';
@@ -552,3 +552,142 @@ it('maps a server scope validation error to the affected selector and focuses it
552
552
  expect(scope).toHaveTextContent('All records');
553
553
  expect(screen.queryByText('Private detail')).not.toBeInTheDocument();
554
554
  });
555
+
556
+ it('does not reset a title-filtered page while its metadata is still loading', async () => {
557
+ let resolveCatalog!: (value: { data: AuthorizationOptionsResponse }) => void;
558
+ const pending = new Promise<{ data: AuthorizationOptionsResponse }>(
559
+ (resolve) => {
560
+ resolveCatalog = resolve;
561
+ },
562
+ );
563
+ const resources = Array.from({ length: 25 }, (_, i) => ({
564
+ ...catalog.sections[0]!.subsections[0]!.resources[0]!,
565
+ id: `resource-${i}`,
566
+ title: `Projects ${i}`,
567
+ }));
568
+ const namedCatalog = {
569
+ ...catalog,
570
+ sections: [
571
+ {
572
+ ...catalog.sections[0]!,
573
+ subsections: [{ ...catalog.sections[0]!.subsections[0]!, resources }],
574
+ },
575
+ ],
576
+ };
577
+ const rules = resources.map((resource, i) => ({
578
+ ...existing,
579
+ key: `rule-${i}`,
580
+ resource: { type: 'composite', id: resource.id },
581
+ }));
582
+ function CurrentLocation() {
583
+ const location = useLocation();
584
+ return <output data-testid='metadata-location'>{location.search}</output>;
585
+ }
586
+ const calls = vi.fn(async ({ path }: { path: string }) =>
587
+ path === 'authorization/permissions'
588
+ ? { data: { unrestricted: true, permissions: [] } }
589
+ : path.endsWith('/options')
590
+ ? pending
591
+ : { data: rules },
592
+ );
593
+ await renderWithApp(
594
+ <>
595
+ <Page />
596
+ <CurrentLocation />
597
+ </>,
598
+ {
599
+ route: '/default-access?q=Projects&page=2&pageSize=20',
600
+ namespace,
601
+ namespaces,
602
+ services,
603
+ fetch: answerApi(calls),
604
+ },
605
+ );
606
+ await waitFor(() =>
607
+ expect(calls).toHaveBeenCalledWith(
608
+ expect.objectContaining({ path: 'authorization/defaultAccess' }),
609
+ ),
610
+ );
611
+ await act(async () => {
612
+ await new Promise((resolve) => setTimeout(resolve, 20));
613
+ });
614
+ expect(screen.getByTestId('metadata-location')).toHaveTextContent('page=2');
615
+ await act(async () => {
616
+ resolveCatalog({ data: namedCatalog });
617
+ });
618
+ expect(await screen.findByText('Page 2 of 2')).toBeVisible();
619
+ expect(screen.getByTestId('metadata-location')).toHaveTextContent('page=2');
620
+ });
621
+
622
+ it('replaces the saved editor history entry and keeps the list query when going back', async () => {
623
+ const query = '?q=Projects&pageSize=20&sort=resource&order=desc';
624
+ function History() {
625
+ const location = useLocation();
626
+ const navigate = useNavigate();
627
+ return (
628
+ <>
629
+ <output data-testid='history-location'>
630
+ {location.pathname}
631
+ {location.search}
632
+ </output>
633
+ <button onClick={() => void navigate(-1)}>Back in history</button>
634
+ </>
635
+ );
636
+ }
637
+ let patches = 0;
638
+ let rules = [existing];
639
+ await renderWithApp(
640
+ <>
641
+ {tree()}
642
+ <History />
643
+ </>,
644
+ {
645
+ route: `/default-access${query}`,
646
+ namespace,
647
+ namespaces,
648
+ services,
649
+ fetch: answerApi((request) => {
650
+ if (request.path === 'authorization/permissions')
651
+ return { data: { unrestricted: true, permissions: [] } };
652
+ if (request.path.endsWith('/options')) return { data: catalog };
653
+ if (request.method === 'PATCH') {
654
+ patches += 1;
655
+ expect(request.json).toEqual({ actions: [existing.actions[0]] });
656
+ rules = [{ ...existing, actions: [existing.actions[0]!] }];
657
+ return { data: rules[0] };
658
+ }
659
+ return { data: rules };
660
+ }),
661
+ },
662
+ );
663
+ await userEvent.click(await screen.findByRole('link', { name: 'Projects' }));
664
+ await userEvent.click(
665
+ await screen.findByRole('combobox', { name: 'Edit · Projects' }),
666
+ );
667
+ await userEvent.click(
668
+ await screen.findByRole('option', {
669
+ name: enUS.settings.defaultAccess.none,
670
+ }),
671
+ );
672
+ const save = screen.getByRole('button', { name: enUS.settings.save });
673
+ expect(save).toBeEnabled();
674
+ await userEvent.click(save);
675
+ await waitFor(() =>
676
+ expect(screen.getByTestId('history-location').textContent).toBe(
677
+ `/default-access${query}`,
678
+ ),
679
+ );
680
+ expect(patches).toBe(1);
681
+ await userEvent.click(
682
+ screen.getByRole('button', { name: 'Back in history' }),
683
+ );
684
+ await waitFor(() =>
685
+ expect(screen.getByTestId('history-location').textContent).toBe(
686
+ `/default-access${query}`,
687
+ ),
688
+ );
689
+ expect(
690
+ screen.queryByRole('button', { name: enUS.settings.save }),
691
+ ).not.toBeInTheDocument();
692
+ expect(patches).toBe(1);
693
+ });