@nocobase/app-template-default 1.0.0-beta.72 → 1.0.0-beta.74

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 (33) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.MD +2 -0
  3. package/client/components/default-access-editor.tsx +183 -0
  4. package/client/components/permission-editor.tsx +216 -150
  5. package/client/components/role-list.tsx +61 -0
  6. package/client/lib/navigation-transform.ts +28 -1
  7. package/client/locales/system/settings-en-US.ts +73 -2
  8. package/client/locales/system/settings-zh-CN.ts +65 -2
  9. package/client/pages/settings/default-access-form.tsx +499 -0
  10. package/client/pages/settings/default-access-model.ts +154 -0
  11. package/client/pages/settings/default-access.tsx +407 -0
  12. package/client/pages/settings/permission-filters.tsx +43 -37
  13. package/client/pages/settings/permission-management-page.tsx +105 -0
  14. package/client/pages/settings/permissions-model.ts +37 -1
  15. package/client/pages/settings/role-form.tsx +46 -18
  16. package/client/pages/settings/roles.tsx +23 -43
  17. package/client/pages/settings/routes.ts +78 -19
  18. package/client/pages/settings/shared.tsx +3 -0
  19. package/client/pages/settings/users.tsx +1 -1
  20. package/client/routing/route-navigation.ts +21 -1
  21. package/package.json +22 -21
  22. package/tests/components/default-access.test.tsx +554 -0
  23. package/tests/components/page-harness.test.tsx +132 -112
  24. package/tests/components/permission-editor.test.tsx +233 -73
  25. package/tests/components/permission-management-tabs.test.tsx +128 -0
  26. package/tests/components/role-list.test.tsx +71 -0
  27. package/tests/components/roles.test.tsx +78 -0
  28. package/tests/components/user-permissions.test.tsx +12 -0
  29. package/tests/logic/client-routes.test.ts +8 -0
  30. package/tests/logic/default-access-model.test.ts +167 -0
  31. package/tests/logic/permission-management-routes.test.ts +64 -0
  32. package/tests/logic/settings-permissions.test.ts +89 -0
  33. package/vitest.config.ts +7 -6
package/AGENTS.md CHANGED
@@ -101,7 +101,7 @@ When reporting or opening this application's address, use the `Local:` URL print
101
101
 
102
102
  Scope all verification — formatting, lint, type checking, tests, builds and runtime checks — to the affected files, projects, or packages and their affected consumers. Do not run full-application or workspace-wide checks, or an aggregate `pnpm check`, as a routine step after each edit. Documentation-only changes need formatting and link checks for the changed documents, not type checking, runtime tests, or builds. Report which checks ran, their scope, and any unverified behavior.
103
103
 
104
- Add tests for what you changed — a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior — in `tests/`, never beside the source; browser tests go in `tests/playwright/`. See [testing and verification](.agents/skills/nocobase-app-development/references/testing.md).
104
+ Add tests for what you changed — a route's authenticated, unauthenticated, and unauthorized responses; a migration's `up` and `down` against a real database; a page's actual behavior — in `tests/`, never beside the source; browser tests go in `tests/playwright/`. Start page tests from `tests/components/page-harness.test.tsx`, which keeps application hooks real and supplies external responses through the test fixture. See [testing and verification](.agents/skills/nocobase-app-development/references/testing.md).
105
105
 
106
106
  For any UI work, start with [the frontend workflow](.agents/skills/nocobase-app-development/references/frontend/ui-workflow.md), which decides the workflow and routes to the styling rules, the theme token contract and the rest.
107
107
 
package/README.MD CHANGED
@@ -401,6 +401,8 @@ An environment variable a section declares takes precedence over the file, and t
401
401
 
402
402
  Tests live in `tests/`, never beside the source they cover. Put logic and integration tests under `tests/logic/` and component tests under `tests/components/`. Browser tests belong in `tests/playwright/`, which Vitest skips. Playwright does not start the application: start it first (`pnpm dev`, or `pnpm build` and then the built application), then run `pnpm test:e2e`; each test reads the application URL, including its base path, from `APP_URL`. Tests that only need a real server, real authentication, or a real database run under Vitest with `createAppTest()` from `@nocobase/app-testing/server`.
403
403
 
404
+ Application tests use the `/server`, `/client` and `/cli` entries of `@nocobase/app-testing`. Start page tests from `tests/components/page-harness.test.tsx`: it uses `renderWithApp()` with real application hooks, strict translations, permission checks and a test toaster. Answer requests through `answerApi()` and replace external services through `services`, without also registering their owning plugins. The template's Vitest configuration includes client plugins in custom package scopes so their contexts and service tokens remain shared. See `.agents/skills/nocobase-app-development/references/testing.md` for fixture ownership and complete server, migration and CLI examples.
405
+
404
406
  ### Notification testing
405
407
 
406
408
  With the `notification:test` `send` permission, send a real message through the durable Notification Manager with `POST <APP_BASE_PATH>/api/notifications/testSends`. `GET <APP_BASE_PATH>/api/notifications/testTargets` lists each configured channel and the fields its test takes: Email tests require an address and IM tests use the configured logical target. The server enforces the permission when the message is submitted.
@@ -0,0 +1,183 @@
1
+ import { useId, type ReactElement, type ReactNode } from 'react';
2
+ import { Input } from '#components/ui/input';
3
+ import { Label } from '#components/ui/label';
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectGroup,
8
+ SelectItem,
9
+ SelectTrigger,
10
+ SelectValue,
11
+ } from '#components/ui/select';
12
+
13
+ export interface DefaultAccessChoice {
14
+ readonly value: string;
15
+ readonly label: string;
16
+ }
17
+ export interface DefaultAccessScopeControl {
18
+ readonly id: string;
19
+ readonly action: string;
20
+ readonly scope: string;
21
+ readonly value: string;
22
+ readonly options: readonly DefaultAccessChoice[];
23
+ readonly configuration?: ReactNode;
24
+ readonly error?: string | undefined;
25
+ }
26
+ export interface DefaultAccessEditorProps {
27
+ readonly ruleKey: string;
28
+ readonly resource: string;
29
+ readonly resources: readonly DefaultAccessChoice[];
30
+ readonly scopes: readonly DefaultAccessScopeControl[];
31
+ readonly existing?: boolean;
32
+ readonly readOnly?: boolean;
33
+ readonly keyError?: string | undefined;
34
+ readonly resourceError?: string | undefined;
35
+ readonly labels: {
36
+ readonly key: string;
37
+ readonly resource: string;
38
+ readonly chooseResource: string;
39
+ readonly action: string;
40
+ readonly scope: string;
41
+ readonly noScopes: string;
42
+ };
43
+ readonly onKeyChange: (value: string) => void;
44
+ readonly onResourceChange: (value: string) => void;
45
+ readonly onScopeChange: (id: string, value: string) => void;
46
+ }
47
+
48
+ /** Controlled fields only: the host owns API authorization, drafts, validation and writes. */
49
+ export function DefaultAccessEditor({
50
+ ruleKey,
51
+ resource,
52
+ resources,
53
+ scopes,
54
+ existing = false,
55
+ readOnly = false,
56
+ keyError,
57
+ resourceError,
58
+ labels,
59
+ onKeyChange,
60
+ onResourceChange,
61
+ onScopeChange,
62
+ }: DefaultAccessEditorProps): ReactElement {
63
+ const id = useId();
64
+ return (
65
+ <div className='flex min-w-0 flex-col gap-6'>
66
+ <div className='grid gap-4 sm:grid-cols-2'>
67
+ <div className='flex min-w-0 flex-col gap-2'>
68
+ <Label htmlFor={`${id}-key`}>{labels.key} *</Label>
69
+ <Input
70
+ id={`${id}-key`}
71
+ name='ruleKey'
72
+ value={ruleKey}
73
+ required
74
+ disabled={existing || readOnly}
75
+ aria-invalid={Boolean(keyError)}
76
+ aria-describedby={keyError ? `${id}-key-error` : undefined}
77
+ onChange={(event) => onKeyChange(event.target.value)}
78
+ />
79
+ {keyError && (
80
+ <p id={`${id}-key-error`} className='text-sm text-destructive'>
81
+ {keyError}
82
+ </p>
83
+ )}
84
+ </div>
85
+ <div className='flex min-w-0 flex-col gap-2'>
86
+ <Label htmlFor={`${id}-resource`}>{labels.resource} *</Label>
87
+ <Select
88
+ value={resource}
89
+ items={resources}
90
+ disabled={existing || readOnly}
91
+ onValueChange={(value) => {
92
+ if (value !== null) onResourceChange(value);
93
+ }}
94
+ >
95
+ <SelectTrigger
96
+ id={`${id}-resource`}
97
+ className='w-full'
98
+ aria-invalid={Boolean(resourceError)}
99
+ aria-describedby={
100
+ resourceError ? `${id}-resource-error` : undefined
101
+ }
102
+ >
103
+ <SelectValue placeholder={labels.chooseResource} />
104
+ </SelectTrigger>
105
+ <SelectContent className='w-auto max-w-[min(var(--container-sm),var(--available-width))] min-w-(--anchor-width) [&_[data-slot=select-item]>:first-child]:whitespace-normal'>
106
+ <SelectGroup>
107
+ {resources.map((option) => (
108
+ <SelectItem key={option.value} value={option.value}>
109
+ {option.label}
110
+ </SelectItem>
111
+ ))}
112
+ </SelectGroup>
113
+ </SelectContent>
114
+ </Select>
115
+ {resourceError && (
116
+ <p id={`${id}-resource-error`} className='text-sm text-destructive'>
117
+ {resourceError}
118
+ </p>
119
+ )}
120
+ </div>
121
+ </div>
122
+ <div className='overflow-hidden rounded-lg border'>
123
+ {scopes.length ? (
124
+ scopes.map((scope) => (
125
+ <div
126
+ key={scope.id}
127
+ className='grid min-w-0 gap-3 border-b p-4 last:border-b-0 sm:grid-cols-3'
128
+ >
129
+ <div className='min-w-0 break-words text-sm font-medium'>
130
+ {scope.action}
131
+ </div>
132
+ <div className='flex min-w-0 flex-col gap-2 sm:col-span-2'>
133
+ <Label htmlFor={`${id}-${scope.id}`}>
134
+ {scope.scope || labels.scope}
135
+ </Label>
136
+ <Select
137
+ value={scope.value}
138
+ items={scope.options}
139
+ disabled={readOnly}
140
+ onValueChange={(value) => {
141
+ if (value !== null) onScopeChange(scope.id, value);
142
+ }}
143
+ >
144
+ <SelectTrigger
145
+ id={`${id}-${scope.id}`}
146
+ className='w-full'
147
+ aria-label={`${scope.action} · ${scope.scope || labels.scope}`}
148
+ aria-invalid={Boolean(scope.error)}
149
+ aria-describedby={
150
+ scope.error ? `${id}-${scope.id}-error` : undefined
151
+ }
152
+ >
153
+ <SelectValue />
154
+ </SelectTrigger>
155
+ <SelectContent className='w-auto max-w-[min(var(--container-sm),var(--available-width))] min-w-(--anchor-width) [&_[data-slot=select-item]>:first-child]:whitespace-normal'>
156
+ <SelectGroup>
157
+ {scope.options.map((option) => (
158
+ <SelectItem key={option.value} value={option.value}>
159
+ {option.label}
160
+ </SelectItem>
161
+ ))}
162
+ </SelectGroup>
163
+ </SelectContent>
164
+ </Select>
165
+ {scope.error && (
166
+ <p
167
+ id={`${id}-${scope.id}-error`}
168
+ className='text-sm text-destructive'
169
+ >
170
+ {scope.error}
171
+ </p>
172
+ )}
173
+ {scope.configuration}
174
+ </div>
175
+ </div>
176
+ ))
177
+ ) : (
178
+ <p className='p-4 text-sm text-muted-foreground'>{labels.noScopes}</p>
179
+ )}
180
+ </div>
181
+ </div>
182
+ );
183
+ }