@nocobase/app-template-default 1.0.0-beta.68 → 1.0.0-beta.69

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 (63) hide show
  1. package/.agents/rules/components.md +1 -1
  2. package/.agents/rules/shell.md +3 -1
  3. package/AGENTS.md +5 -3
  4. package/Dockerfile.dockerignore +0 -1
  5. package/README.MD +3 -1
  6. package/client/app.ts +15 -0
  7. package/client/components/data-table/column-header.tsx +91 -0
  8. package/client/components/data-table/data-table.tsx +216 -0
  9. package/client/components/data-table/index.ts +13 -0
  10. package/client/components/data-table/pagination.tsx +165 -0
  11. package/client/components/data-table/view-options.tsx +77 -0
  12. package/client/components/permission-editor.tsx +589 -0
  13. package/client/components/permission-filter.tsx +455 -0
  14. package/client/components/route-overlay.tsx +26 -13
  15. package/client/components/ui/checkbox.tsx +25 -0
  16. package/client/components/ui/sidebar.tsx +1 -4
  17. package/client/components/ui/switch.tsx +30 -0
  18. package/client/components/ui/table.tsx +113 -0
  19. package/client/layouts/components/app-sidebar.tsx +1 -1
  20. package/client/layouts/use-sidebar-preference.ts +62 -42
  21. package/client/locales/system/en-US.ts +7 -0
  22. package/client/locales/system/settings-en-US.ts +141 -0
  23. package/client/locales/system/settings-zh-CN.ts +135 -0
  24. package/client/locales/system/zh-CN.ts +7 -0
  25. package/client/pages/auth/auth-page.css +15 -0
  26. package/client/pages/auth/login.tsx +1 -1
  27. package/client/pages/auth/shared.tsx +3 -1
  28. package/client/pages/settings/hooks.tsx +137 -0
  29. package/client/pages/settings/permission-filters.tsx +119 -0
  30. package/client/pages/settings/permissions-model.ts +260 -0
  31. package/client/pages/settings/role-form.tsx +426 -0
  32. package/client/pages/settings/roles.tsx +163 -0
  33. package/client/pages/settings/routes.ts +79 -0
  34. package/client/pages/settings/shared.tsx +119 -0
  35. package/client/pages/settings/user-detail.tsx +268 -0
  36. package/client/pages/settings/user-form.tsx +427 -0
  37. package/client/pages/settings/users.tsx +451 -0
  38. package/client/routing/app-router.tsx +4 -1
  39. package/client/routing/session-preparation.tsx +65 -0
  40. package/client/styles.css +4 -7
  41. package/package.json +24 -23
  42. package/tests/components/app-layout-routes.test.tsx +14 -7
  43. package/tests/components/data-table.test.tsx +127 -0
  44. package/tests/components/layout-i18n.test.tsx +8 -1
  45. package/tests/components/navigation-menu.test.tsx +24 -14
  46. package/tests/components/permission-editor.test.tsx +477 -0
  47. package/tests/components/permission-filter-binding.test.tsx +150 -0
  48. package/tests/components/permission-filter.test.tsx +202 -0
  49. package/tests/components/settings.test.tsx +294 -0
  50. package/tests/components/sidebar-permissions.test.tsx +12 -4
  51. package/tests/components/sidebar-preference.test.tsx +209 -14
  52. package/tests/components/user-permissions.test.tsx +142 -0
  53. package/tests/fixtures/theme-tokens.tsx +9 -1
  54. package/tests/logic/asset-url-build.test.ts +0 -1
  55. package/tests/logic/client-routes.test.ts +40 -0
  56. package/tests/logic/client-shell.test.tsx +10 -2
  57. package/tests/logic/config-example.test.ts +30 -0
  58. package/tests/logic/dev-client-watch.test.ts +8 -1
  59. package/tests/logic/route-overlay.test.tsx +42 -0
  60. package/tests/logic/session-preparation.test.tsx +223 -0
  61. package/tests/logic/settings-permissions.test.ts +168 -0
  62. package/vite.config.ts +0 -22
  63. package/client/agent-annotations-host.ts +0 -25
@@ -10,6 +10,10 @@ import { MemoryRouter, Route, Routes } from 'react-router';
10
10
  import { afterEach, beforeEach, expect, it, vi } from 'vitest';
11
11
 
12
12
  import { AppLayout } from '../../client/layouts/app-layout.js';
13
+ import {
14
+ createAppSidebarPreferenceStore,
15
+ SidebarPreferenceProvider,
16
+ } from '../../client/layouts/use-sidebar-preference.js';
13
17
  import enUS from '../../client/locales/en-US.js';
14
18
 
15
19
  // Breadcrumb titles are route data, not keys any namespace owns: the trail translates each through its package's
@@ -87,14 +91,17 @@ function OrderPage(): ReactElement {
87
91
  }
88
92
 
89
93
  function renderLayout(page: ReactElement): HTMLElement {
94
+ const sidebarPreference = createAppSidebarPreferenceStore('/test-app');
90
95
  render(
91
- <MemoryRouter initialEntries={['/orders/42']}>
92
- <Routes>
93
- <Route element={<AppLayout routes={routes} />}>
94
- <Route path='/orders/:id' element={page} />
95
- </Route>
96
- </Routes>
97
- </MemoryRouter>,
96
+ <SidebarPreferenceProvider value={sidebarPreference}>
97
+ <MemoryRouter initialEntries={['/orders/42']}>
98
+ <Routes>
99
+ <Route element={<AppLayout routes={routes} />}>
100
+ <Route path='/orders/:id' element={page} />
101
+ </Route>
102
+ </Routes>
103
+ </MemoryRouter>
104
+ </SidebarPreferenceProvider>,
98
105
  { wrapper: I18n },
99
106
  );
100
107
  return screen.getByRole('banner');
@@ -0,0 +1,127 @@
1
+ import { renderWithApp } from '@nocobase/app-testing/client';
2
+ import type { ColumnDef, PaginationState } from '@tanstack/react-table';
3
+ import { screen, within } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { useState } from 'react';
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ DataTable,
9
+ DataTableColumnHeader,
10
+ DataTableViewOptions,
11
+ } from '../../client/components/data-table/index.js';
12
+ import enUS from '../../client/locales/en-US.js';
13
+ import zhCN from '../../client/locales/zh-CN.js';
14
+ const namespace = '@nocobase/datatable-test';
15
+ const namespaces = {
16
+ [namespace]: {
17
+ 'en-US': async () => ({ default: enUS }),
18
+ 'zh-CN': async () => ({ default: zhCN }),
19
+ },
20
+ };
21
+ type Item = { id: string; amount: number };
22
+ const rows: Item[] = Array.from({ length: 12 }, (_, amount) => ({
23
+ id: `row-${amount}`,
24
+ amount,
25
+ }));
26
+ const columns: ColumnDef<Item>[] = [
27
+ { accessorKey: 'id', header: 'ID', enableHiding: false },
28
+ {
29
+ accessorKey: 'amount',
30
+ header: ({ column }) => (
31
+ <DataTableColumnHeader column={column} title='Amount' />
32
+ ),
33
+ },
34
+ ];
35
+ describe('standard DataTable', () => {
36
+ it('uses real TanStack sorting, pagination and column visibility with restoration', async () => {
37
+ const user = userEvent.setup();
38
+ await renderWithApp(
39
+ <DataTable
40
+ columns={columns}
41
+ data={rows}
42
+ getRowId={(row) => row.id}
43
+ showSelectedCount={false}
44
+ toolbar={(table) => (
45
+ <DataTableViewOptions table={table} getColumnLabel={() => 'Amount'} />
46
+ )}
47
+ />,
48
+ { namespace, namespaces },
49
+ );
50
+ expect(screen.getByText('Page 1 of 2')).toBeVisible();
51
+ expect(screen.queryByText('row-10')).not.toBeInTheDocument();
52
+ await user.click(screen.getByRole('button', { name: 'Go to next page' }));
53
+ expect(screen.getByText('row-10')).toBeVisible();
54
+ await user.click(
55
+ screen.getByRole('button', { name: 'Go to previous page' }),
56
+ );
57
+ await user.click(
58
+ screen.getByRole('button', { name: 'Amount', exact: true }),
59
+ );
60
+ await user.click(
61
+ await screen.findByRole('menuitem', { name: 'Desc', exact: true }),
62
+ );
63
+ expect(
64
+ within(screen.getAllByRole('row')[1]!).getByText('row-11'),
65
+ ).toBeVisible();
66
+ await user.click(screen.getByRole('button', { name: 'View', exact: true }));
67
+ await user.click(
68
+ await screen.findByRole('menuitemcheckbox', {
69
+ name: 'Amount',
70
+ exact: true,
71
+ }),
72
+ );
73
+ expect(screen.getAllByRole('columnheader', { hidden: true })).toHaveLength(
74
+ 1,
75
+ );
76
+ await user.click(
77
+ await screen.findByRole('menuitemcheckbox', {
78
+ name: 'Amount',
79
+ exact: true,
80
+ }),
81
+ );
82
+ expect(screen.getByRole('columnheader', { name: 'Amount' })).toBeVisible();
83
+ });
84
+ it('preserves the server page and sort instead of applying client paging or sorting again', async () => {
85
+ const user = userEvent.setup();
86
+ const pageRows = rows.slice(0, 2);
87
+ function ServerPage() {
88
+ const [page, setPage] = useState<PaginationState>({
89
+ pageIndex: 2,
90
+ pageSize: 2,
91
+ });
92
+ return (
93
+ <DataTable
94
+ columns={columns}
95
+ data={pageRows}
96
+ manualPagination
97
+ rowCount={12}
98
+ paginationState={page}
99
+ onPaginationChange={setPage}
100
+ manualSorting
101
+ sortingState={[{ id: 'amount', desc: true }]}
102
+ showSelectedCount={false}
103
+ pageSizeOptions={[2]}
104
+ />
105
+ );
106
+ }
107
+ await renderWithApp(<ServerPage />, { namespace, namespaces });
108
+ expect(screen.getByText('Page 3 of 6')).toBeVisible();
109
+ expect(screen.getByText('row-0')).toBeVisible();
110
+ expect(
111
+ within(screen.getAllByRole('row')[1]!).getByText('row-0'),
112
+ ).toBeVisible();
113
+ await user.click(screen.getByRole('button', { name: 'Go to next page' }));
114
+ expect(screen.getByText('Page 4 of 6')).toBeVisible();
115
+ expect(screen.getByText('row-0')).toBeVisible();
116
+ });
117
+ it('keeps empty-state and paging controls localized in Chinese', async () => {
118
+ await renderWithApp(
119
+ <DataTable columns={columns} data={[]} showSelectedCount={false} />,
120
+ { namespace, namespaces, locale: 'zh-CN' },
121
+ );
122
+ expect(screen.getByText(zhCN.dataTable.noResults)).toBeVisible();
123
+ expect(
124
+ screen.getByRole('button', { name: zhCN.dataTable.nextPage }),
125
+ ).toBeDisabled();
126
+ });
127
+ });
@@ -13,6 +13,10 @@ import { describe, expect, it, vi } from 'vitest';
13
13
 
14
14
  import locales from '../../client/locales/index.js';
15
15
  import { AppLayout } from '../../client/layouts/app-layout.js';
16
+ import {
17
+ createAppSidebarPreferenceStore,
18
+ SidebarPreferenceProvider,
19
+ } from '../../client/layouts/use-sidebar-preference.js';
16
20
  vi.mock('@nocobase/app-plugin-i18n/client', () => ({
17
21
  useSyncServerLocale: () => {},
18
22
  }));
@@ -45,6 +49,7 @@ const overview: AppClientRegisteredRoute = {
45
49
  };
46
50
 
47
51
  async function setup(children: ReactNode) {
52
+ const sidebarPreference = createAppSidebarPreferenceStore('/test-app');
48
53
  vi.stubGlobal('matchMedia', () => ({
49
54
  matches: true,
50
55
  addEventListener: vi.fn(),
@@ -64,7 +69,9 @@ async function setup(children: ReactNode) {
64
69
  render(
65
70
  <I18nProvider runtime={runtime}>
66
71
  <ClientApplicationContext.Provider value={app}>
67
- <MemoryRouter initialEntries={['/']}>{children}</MemoryRouter>
72
+ <SidebarPreferenceProvider value={sidebarPreference}>
73
+ <MemoryRouter initialEntries={['/']}>{children}</MemoryRouter>
74
+ </SidebarPreferenceProvider>
68
75
  </ClientApplicationContext.Provider>
69
76
  </I18nProvider>,
70
77
  );
@@ -1,3 +1,7 @@
1
+ import {
2
+ createAppSidebarPreferenceStore,
3
+ SidebarPreferenceProvider,
4
+ } from '../../client/layouts/use-sidebar-preference.js';
1
5
  import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
2
6
  import {
3
7
  act,
@@ -38,11 +42,15 @@ function I18n({ children }: { readonly children: ReactNode }) {
38
42
  return <TestI18nProvider runtime={runtime}>{children}</TestI18nProvider>;
39
43
  }
40
44
 
41
- const preferenceKey = 'nocobase:sidebar:collapsed';
45
+ const sidebarPreference = createAppSidebarPreferenceStore('/main');
46
+
47
+ const preferenceKey = 'nocobase:sidebar:collapsed:app:/main';
42
48
 
43
49
  function setCollapsed(collapsed: boolean) {
44
50
  localStorage.setItem(preferenceKey, String(collapsed));
45
- window.dispatchEvent(new Event('nocobase:sidebar-preference-change'));
51
+ window.dispatchEvent(
52
+ new Event('nocobase:sidebar-preference-change:app:/main'),
53
+ );
46
54
  }
47
55
 
48
56
  function page(name: string): AppClientRegisteredRoute {
@@ -69,18 +77,20 @@ function shell(
69
77
  ) {
70
78
  return (
71
79
  <MemoryRouter>
72
- <AppSidebarProvider>
73
- <AppSidebar label='Navigation'>
74
- <NavigationMenu
75
- items={items}
76
- label='Menu'
77
- selectedKey={selectedKey}
78
- sections={sections}
79
- badges={badges}
80
- />
81
- </AppSidebar>
82
- <AppSidebarToggle />
83
- </AppSidebarProvider>
80
+ <SidebarPreferenceProvider value={sidebarPreference}>
81
+ <AppSidebarProvider>
82
+ <AppSidebar label='Navigation'>
83
+ <NavigationMenu
84
+ items={items}
85
+ label='Menu'
86
+ selectedKey={selectedKey}
87
+ sections={sections}
88
+ badges={badges}
89
+ />
90
+ </AppSidebar>
91
+ <AppSidebarToggle />
92
+ </AppSidebarProvider>
93
+ </SidebarPreferenceProvider>
84
94
  </MemoryRouter>
85
95
  );
86
96
  }