@nocobase/app-template-default 1.0.0-beta.73 → 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.
- package/AGENTS.md +1 -1
- package/README.MD +2 -0
- package/client/components/default-access-editor.tsx +183 -0
- package/client/components/permission-editor.tsx +216 -150
- package/client/components/role-list.tsx +61 -0
- package/client/lib/navigation-transform.ts +28 -1
- package/client/locales/system/settings-en-US.ts +73 -2
- package/client/locales/system/settings-zh-CN.ts +65 -2
- package/client/pages/settings/default-access-form.tsx +499 -0
- package/client/pages/settings/default-access-model.ts +154 -0
- package/client/pages/settings/default-access.tsx +407 -0
- package/client/pages/settings/permission-filters.tsx +43 -37
- package/client/pages/settings/permission-management-page.tsx +105 -0
- package/client/pages/settings/permissions-model.ts +37 -1
- package/client/pages/settings/role-form.tsx +46 -18
- package/client/pages/settings/roles.tsx +23 -43
- package/client/pages/settings/routes.ts +78 -19
- package/client/pages/settings/shared.tsx +3 -0
- package/client/pages/settings/users.tsx +1 -1
- package/client/routing/route-navigation.ts +21 -1
- package/package.json +19 -19
- package/tests/components/default-access.test.tsx +554 -0
- package/tests/components/page-harness.test.tsx +132 -112
- package/tests/components/permission-editor.test.tsx +233 -73
- package/tests/components/permission-management-tabs.test.tsx +128 -0
- package/tests/components/role-list.test.tsx +71 -0
- package/tests/components/roles.test.tsx +78 -0
- package/tests/components/user-permissions.test.tsx +12 -0
- package/tests/logic/client-routes.test.ts +8 -0
- package/tests/logic/default-access-model.test.ts +167 -0
- package/tests/logic/permission-management-routes.test.ts +64 -0
- package/tests/logic/settings-permissions.test.ts +89 -0
- package/vitest.config.ts +2 -2
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
|
+
}
|