@nocobase/app-template-default 1.0.0-beta.74 → 1.0.0-beta.76
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/rules/build-and-deploy.md +4 -0
- package/.agents/rules/components.md +3 -1
- package/AGENTS.md +7 -1
- package/README.MD +3 -3
- package/cli/plugins.ts +2 -1
- package/client/components/data-table/data-table.tsx +2 -0
- package/client/components/data-table/pagination.tsx +6 -6
- package/client/components/flow-diagram.tsx +645 -83
- package/client/components/flow-layout.ts +117 -7
- package/client/components/lifecycle-diagram.tsx +175 -37
- package/client/components/record-link.tsx +20 -0
- package/client/components/required-role-setup.tsx +93 -0
- package/client/components/role-list.tsx +71 -31
- package/client/components/setup-readiness-state.ts +135 -0
- package/client/components/setup-readiness.tsx +91 -0
- package/client/components/ui/combobox.tsx +300 -0
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
- package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
- package/client/hooks/use-table-url-state.ts +93 -0
- package/client/lib/navigation-transform.ts +2 -1
- package/client/lib/required-role-setup.ts +98 -0
- package/client/lib/return-to.ts +19 -0
- package/client/locales/system/en-US.ts +10 -0
- package/client/locales/system/settings-en-US.ts +38 -1
- package/client/locales/system/settings-zh-CN.ts +34 -2
- package/client/locales/system/zh-CN.ts +10 -0
- package/client/pages/settings/default-access-form.tsx +4 -1
- package/client/pages/settings/default-access.tsx +40 -8
- package/client/pages/settings/role-form.tsx +1 -1
- package/client/pages/settings/roles.tsx +47 -6
- package/client/pages/settings/shared.tsx +15 -13
- package/client/pages/settings/user-detail.tsx +15 -259
- package/client/pages/settings/user-form.tsx +299 -147
- package/client/pages/settings/users.tsx +211 -123
- package/client/plugins.ts +2 -0
- package/config.example.yml +6 -5
- package/package.json +16 -14
- package/server/plugins.ts +2 -0
- package/tests/components/default-access.test.tsx +141 -2
- package/tests/components/role-list.test.tsx +73 -8
- package/tests/components/roles.test.tsx +14 -7
- package/tests/components/settings-continuity.test.tsx +426 -0
- package/tests/components/settings-feedback.test.tsx +304 -0
- package/tests/components/settings.test.tsx +1 -1
- package/tests/components/setup-readiness.test.tsx +370 -0
- package/tests/components/table-url-state.test.tsx +95 -0
- package/tests/components/user-drawer.test.tsx +526 -0
- package/tests/logic/authorization-assignment-safety.test.ts +82 -0
- package/tests/logic/foundation-integration.test.ts +62 -0
- package/tests/logic/return-to.test.ts +19 -0
- package/tsconfig.json +1 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { RecordLink } from '#components/record-link';
|
|
2
|
+
import { BackButton } from '#components/back-button';
|
|
3
|
+
import { safeReturnTo } from '../../lib/return-to.js';
|
|
4
|
+
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
1
5
|
import { DataTable, DataTableColumnHeader } from '#components/data-table';
|
|
2
6
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
3
7
|
import { useSettingsData, useUserPermission } from './hooks.js';
|
|
@@ -6,21 +10,29 @@ import { useTranslation } from '@nocobase/i18n/client';
|
|
|
6
10
|
import {
|
|
7
11
|
UsersClient,
|
|
8
12
|
type ManagedUser,
|
|
13
|
+
type UsersOptions,
|
|
9
14
|
} from '@nocobase/app-plugin-users/client/user-client';
|
|
10
15
|
import {
|
|
11
16
|
useCallback,
|
|
17
|
+
useRef,
|
|
12
18
|
useMemo,
|
|
13
19
|
useState,
|
|
14
20
|
useEffect,
|
|
15
21
|
type ReactElement,
|
|
16
22
|
} from 'react';
|
|
17
|
-
import {
|
|
18
|
-
|
|
23
|
+
import {
|
|
24
|
+
Link,
|
|
25
|
+
Outlet,
|
|
26
|
+
useLocation,
|
|
27
|
+
useSearchParams,
|
|
28
|
+
useResolvedPath,
|
|
29
|
+
} from 'react-router';
|
|
30
|
+
import { PlusIcon, UsersIcon } from 'lucide-react';
|
|
19
31
|
import { PageContainer } from '#components/page-container';
|
|
20
32
|
import { PageHeader } from '#components/page-header';
|
|
21
33
|
import { Button } from '#components/ui/button';
|
|
22
34
|
import { Input } from '#components/ui/input';
|
|
23
|
-
import {
|
|
35
|
+
import { StatusBadge } from '#components/status-badge';
|
|
24
36
|
import {
|
|
25
37
|
Select,
|
|
26
38
|
SelectTrigger,
|
|
@@ -28,13 +40,6 @@ import {
|
|
|
28
40
|
SelectContent,
|
|
29
41
|
SelectItem,
|
|
30
42
|
} from '#components/ui/select';
|
|
31
|
-
import {
|
|
32
|
-
DropdownMenu,
|
|
33
|
-
DropdownMenuTrigger,
|
|
34
|
-
DropdownMenuContent,
|
|
35
|
-
DropdownMenuItem,
|
|
36
|
-
DropdownMenuSeparator,
|
|
37
|
-
} from '#components/ui/dropdown-menu';
|
|
38
43
|
import {
|
|
39
44
|
Empty,
|
|
40
45
|
EmptyMedia,
|
|
@@ -44,6 +49,19 @@ import {
|
|
|
44
49
|
} from '#components/ui/empty';
|
|
45
50
|
import { ConfirmAction, SettingsError, SettingsLoading } from './shared.js';
|
|
46
51
|
|
|
52
|
+
function UserName({ user }: { readonly user: ManagedUser }): ReactElement {
|
|
53
|
+
const canUpdate = useUserPermission('update', user.id);
|
|
54
|
+
const canAssign = useUserPermission('assign-role', user.id);
|
|
55
|
+
const location = useLocation();
|
|
56
|
+
return canUpdate || canAssign ? (
|
|
57
|
+
<RecordLink to={{ pathname: `${user.id}/edit`, search: location.search }}>
|
|
58
|
+
{user.name}
|
|
59
|
+
</RecordLink>
|
|
60
|
+
) : (
|
|
61
|
+
<span className='font-medium'>{user.name}</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
function UserActions({
|
|
48
66
|
user,
|
|
49
67
|
reload,
|
|
@@ -55,11 +73,9 @@ function UserActions({
|
|
|
55
73
|
const api = useApiClient();
|
|
56
74
|
const toaster = useToaster();
|
|
57
75
|
const client = useMemo(() => new UsersClient(api), [api]);
|
|
58
|
-
const canUpdate = useUserPermission('update', user.id);
|
|
59
76
|
const canDisable = useUserPermission('disable', user.id);
|
|
60
77
|
const canEnable = useUserPermission('enable', user.id);
|
|
61
78
|
const canRevoke = useUserPermission('revoke-sessions', user.id);
|
|
62
|
-
const location = useLocation();
|
|
63
79
|
const [action, setAction] = useState<
|
|
64
80
|
'disable' | 'enable' | 'revokeSessions'
|
|
65
81
|
>();
|
|
@@ -73,7 +89,7 @@ function UserActions({
|
|
|
73
89
|
const updated = await client[action](user.id);
|
|
74
90
|
toaster.show({
|
|
75
91
|
type: 'success',
|
|
76
|
-
title: t(
|
|
92
|
+
title: t(`settings.userActionSaved.${action}`, { name: user.name }),
|
|
77
93
|
});
|
|
78
94
|
setAction(undefined);
|
|
79
95
|
reload(updated ?? undefined);
|
|
@@ -83,56 +99,32 @@ function UserActions({
|
|
|
83
99
|
setBusy(false);
|
|
84
100
|
}
|
|
85
101
|
};
|
|
102
|
+
if (!canRevoke && !(user.disabledAt ? canEnable : canDisable)) return <></>;
|
|
86
103
|
return (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)}
|
|
112
|
-
{(canDisable || canEnable || canRevoke) && <DropdownMenuSeparator />}
|
|
113
|
-
{!user.disabledAt && canDisable && (
|
|
114
|
-
<DropdownMenuItem
|
|
115
|
-
variant='destructive'
|
|
116
|
-
onClick={() => setAction('disable')}
|
|
117
|
-
>
|
|
118
|
-
{t('settings.disable')}
|
|
119
|
-
</DropdownMenuItem>
|
|
120
|
-
)}
|
|
121
|
-
{user.disabledAt && canEnable && (
|
|
122
|
-
<DropdownMenuItem onClick={() => setAction('enable')}>
|
|
123
|
-
{t('settings.enable')}
|
|
124
|
-
</DropdownMenuItem>
|
|
125
|
-
)}
|
|
126
|
-
{canRevoke && (
|
|
127
|
-
<DropdownMenuItem
|
|
128
|
-
variant='destructive'
|
|
129
|
-
onClick={() => setAction('revokeSessions')}
|
|
130
|
-
>
|
|
131
|
-
{t('settings.revokeSessions')}
|
|
132
|
-
</DropdownMenuItem>
|
|
133
|
-
)}
|
|
134
|
-
</DropdownMenuContent>
|
|
135
|
-
</DropdownMenu>
|
|
104
|
+
<div className='flex items-center justify-end gap-2 whitespace-nowrap'>
|
|
105
|
+
{!user.disabledAt && canDisable && (
|
|
106
|
+
<Button
|
|
107
|
+
variant='outline'
|
|
108
|
+
size='xs'
|
|
109
|
+
onClick={() => setAction('disable')}
|
|
110
|
+
>
|
|
111
|
+
{t('settings.disable')}
|
|
112
|
+
</Button>
|
|
113
|
+
)}
|
|
114
|
+
{user.disabledAt && canEnable && (
|
|
115
|
+
<Button variant='outline' size='xs' onClick={() => setAction('enable')}>
|
|
116
|
+
{t('settings.enable')}
|
|
117
|
+
</Button>
|
|
118
|
+
)}
|
|
119
|
+
{canRevoke && (
|
|
120
|
+
<Button
|
|
121
|
+
variant='outline'
|
|
122
|
+
size='xs'
|
|
123
|
+
onClick={() => setAction('revokeSessions')}
|
|
124
|
+
>
|
|
125
|
+
{t('settings.revokeSessions')}
|
|
126
|
+
</Button>
|
|
127
|
+
)}
|
|
136
128
|
<ConfirmAction
|
|
137
129
|
open={Boolean(action)}
|
|
138
130
|
title={t(
|
|
@@ -158,7 +150,7 @@ function UserActions({
|
|
|
158
150
|
onCancel={() => setAction(undefined)}
|
|
159
151
|
onConfirm={() => void run()}
|
|
160
152
|
/>
|
|
161
|
-
|
|
153
|
+
</div>
|
|
162
154
|
);
|
|
163
155
|
}
|
|
164
156
|
|
|
@@ -166,6 +158,24 @@ export default function UsersPage(): ReactElement {
|
|
|
166
158
|
const { t } = useTranslation();
|
|
167
159
|
const [params, setParams] = useSearchParams();
|
|
168
160
|
const location = useLocation();
|
|
161
|
+
const listPath = useResolvedPath('.').pathname;
|
|
162
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
163
|
+
const previousPathRef = useRef(location.pathname);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const returned =
|
|
166
|
+
previousPathRef.current !== location.pathname &&
|
|
167
|
+
location.pathname === listPath;
|
|
168
|
+
previousPathRef.current = location.pathname;
|
|
169
|
+
if (!returned) return;
|
|
170
|
+
const frame = requestAnimationFrame(() => {
|
|
171
|
+
if (
|
|
172
|
+
document.activeElement === document.body ||
|
|
173
|
+
!document.activeElement?.isConnected
|
|
174
|
+
)
|
|
175
|
+
searchRef.current?.focus();
|
|
176
|
+
});
|
|
177
|
+
return () => cancelAnimationFrame(frame);
|
|
178
|
+
}, [listPath, location.pathname]);
|
|
169
179
|
const [queryDraft, setQueryDraft] = useState<{
|
|
170
180
|
key: string;
|
|
171
181
|
value: string;
|
|
@@ -177,7 +187,43 @@ export default function UsersPage(): ReactElement {
|
|
|
177
187
|
const setQuery = (value: string) =>
|
|
178
188
|
setQueryDraft({ key: location.key, value });
|
|
179
189
|
const [composing, setComposing] = useState(false);
|
|
180
|
-
const
|
|
190
|
+
const options = useSettingsData<{ data: UsersOptions }>('users/options');
|
|
191
|
+
const roleScopes = useMemo(
|
|
192
|
+
() => options.data?.data.roleScopes ?? [],
|
|
193
|
+
[options.data],
|
|
194
|
+
);
|
|
195
|
+
const returnTo = safeReturnTo(params.get('returnTo'));
|
|
196
|
+
const roleScope = params.get('roleScope') ?? '';
|
|
197
|
+
const role = params.get('role') ?? '';
|
|
198
|
+
const roleText = useCallback(
|
|
199
|
+
(option: { label: string; labelI18nKey?: string; labelI18nNs?: string }) =>
|
|
200
|
+
option.labelI18nKey
|
|
201
|
+
? t(option.labelI18nKey, {
|
|
202
|
+
ns: option.labelI18nNs,
|
|
203
|
+
defaultValue: option.label,
|
|
204
|
+
})
|
|
205
|
+
: option.label,
|
|
206
|
+
[t],
|
|
207
|
+
);
|
|
208
|
+
const roleChoices = roleScopes.flatMap((scope) =>
|
|
209
|
+
scope.options.map((option) => ({
|
|
210
|
+
value: JSON.stringify([scope.key, option.value]),
|
|
211
|
+
label: `${roleText(scope)} · ${roleText(option)}`,
|
|
212
|
+
})),
|
|
213
|
+
);
|
|
214
|
+
const selectedRole =
|
|
215
|
+
roleScope && role ? JSON.stringify([roleScope, role]) : 'all';
|
|
216
|
+
if (
|
|
217
|
+
selectedRole !== 'all' &&
|
|
218
|
+
!roleChoices.some((option) => option.value === selectedRole)
|
|
219
|
+
)
|
|
220
|
+
roleChoices.push({ value: selectedRole, label: `${roleScope} · ${role}` });
|
|
221
|
+
const page = Math.max(
|
|
222
|
+
1,
|
|
223
|
+
Number.isSafeInteger(Number(params.get('page')))
|
|
224
|
+
? Number(params.get('page'))
|
|
225
|
+
: 1,
|
|
226
|
+
);
|
|
181
227
|
const status = params.get('status') ?? 'all';
|
|
182
228
|
const pageSize = [20, 50, 100].includes(Number(params.get('pageSize')))
|
|
183
229
|
? Number(params.get('pageSize'))
|
|
@@ -215,6 +261,8 @@ export default function UsersPage(): ReactElement {
|
|
|
215
261
|
pageSize: String(pageSize),
|
|
216
262
|
...(params.get('q') ? { q: params.get('q')! } : {}),
|
|
217
263
|
...(status !== 'all' ? { status } : {}),
|
|
264
|
+
...(roleScope ? { roleScope } : {}),
|
|
265
|
+
...(role ? { role } : {}),
|
|
218
266
|
...(params.get('order')
|
|
219
267
|
? { sort: 'name', order: params.get('order')! }
|
|
220
268
|
: {}),
|
|
@@ -223,6 +271,25 @@ export default function UsersPage(): ReactElement {
|
|
|
223
271
|
data: readonly ManagedUser[];
|
|
224
272
|
meta: { total: number };
|
|
225
273
|
}>(`users?${remote}`, true);
|
|
274
|
+
const tableState = useTableUrlState({
|
|
275
|
+
defaultPageSize: 20,
|
|
276
|
+
pageSizeOptions: [20, 50, 100],
|
|
277
|
+
sortColumns: ['name'],
|
|
278
|
+
rowCount:
|
|
279
|
+
!request.loading && !request.error ? request.data?.meta.total : undefined,
|
|
280
|
+
});
|
|
281
|
+
const clearFilters = () => {
|
|
282
|
+
setQuery('');
|
|
283
|
+
setParams(
|
|
284
|
+
(old) => {
|
|
285
|
+
const next = new URLSearchParams(old);
|
|
286
|
+
for (const key of ['q', 'status', 'roleScope', 'role', 'page'])
|
|
287
|
+
next.delete(key);
|
|
288
|
+
return next;
|
|
289
|
+
},
|
|
290
|
+
{ replace: true },
|
|
291
|
+
);
|
|
292
|
+
};
|
|
226
293
|
const { data: pageData, replaceData, reload: refreshUsers } = request;
|
|
227
294
|
const reload = useCallback(
|
|
228
295
|
(updated?: ManagedUser) => {
|
|
@@ -234,8 +301,10 @@ export default function UsersPage(): ReactElement {
|
|
|
234
301
|
),
|
|
235
302
|
});
|
|
236
303
|
refreshUsers();
|
|
304
|
+
if (updated && location.pathname === listPath)
|
|
305
|
+
requestAnimationFrame(() => searchRef.current?.focus());
|
|
237
306
|
},
|
|
238
|
-
[pageData, replaceData, refreshUsers],
|
|
307
|
+
[pageData, replaceData, refreshUsers, location.pathname, listPath],
|
|
239
308
|
);
|
|
240
309
|
const canCreate = useUserPermission('create');
|
|
241
310
|
const canAssign = useUserPermission('assign-role');
|
|
@@ -248,17 +317,7 @@ export default function UsersPage(): ReactElement {
|
|
|
248
317
|
<DataTableColumnHeader column={column} title={t('settings.name')} />
|
|
249
318
|
),
|
|
250
319
|
enableHiding: false,
|
|
251
|
-
cell: ({ row }) =>
|
|
252
|
-
<Link
|
|
253
|
-
className='font-medium text-foreground underline-offset-4 hover:underline'
|
|
254
|
-
to={{
|
|
255
|
-
pathname: row.original.id,
|
|
256
|
-
search: location.search,
|
|
257
|
-
}}
|
|
258
|
-
>
|
|
259
|
-
{row.original.name}
|
|
260
|
-
</Link>
|
|
261
|
-
),
|
|
320
|
+
cell: ({ row }) => <UserName user={row.original} />,
|
|
262
321
|
},
|
|
263
322
|
{
|
|
264
323
|
accessorKey: 'email',
|
|
@@ -275,13 +334,13 @@ export default function UsersPage(): ReactElement {
|
|
|
275
334
|
header: t('settings.status'),
|
|
276
335
|
enableSorting: false,
|
|
277
336
|
cell: ({ row }) => (
|
|
278
|
-
<
|
|
337
|
+
<StatusBadge tone={row.original.disabledAt ? 'neutral' : 'success'}>
|
|
279
338
|
{t(
|
|
280
339
|
row.original.disabledAt
|
|
281
340
|
? 'settings.disabled'
|
|
282
341
|
: 'settings.enabled',
|
|
283
342
|
)}
|
|
284
|
-
</
|
|
343
|
+
</StatusBadge>
|
|
285
344
|
),
|
|
286
345
|
},
|
|
287
346
|
{
|
|
@@ -289,7 +348,19 @@ export default function UsersPage(): ReactElement {
|
|
|
289
348
|
header: t('settings.assignedRoles'),
|
|
290
349
|
enableSorting: false,
|
|
291
350
|
cell: ({ row }) =>
|
|
292
|
-
Object.
|
|
351
|
+
Object.entries(row.original.roleScopes)
|
|
352
|
+
.flatMap(([scopeKey, value]) => {
|
|
353
|
+
const scope = roleScopes.find((entry) => entry.key === scopeKey);
|
|
354
|
+
return (typeof value === 'string' ? [value] : value).map(
|
|
355
|
+
(key) => {
|
|
356
|
+
const option = scope?.options.find(
|
|
357
|
+
(entry) => entry.value === key,
|
|
358
|
+
);
|
|
359
|
+
return option ? roleText(option) : key;
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
})
|
|
363
|
+
.join(', ') || '—',
|
|
293
364
|
},
|
|
294
365
|
{
|
|
295
366
|
id: 'actions',
|
|
@@ -298,7 +369,7 @@ export default function UsersPage(): ReactElement {
|
|
|
298
369
|
cell: ({ row }) => <UserActions user={row.original} reload={reload} />,
|
|
299
370
|
},
|
|
300
371
|
],
|
|
301
|
-
[t,
|
|
372
|
+
[t, reload, roleScopes, roleText],
|
|
302
373
|
);
|
|
303
374
|
const create =
|
|
304
375
|
canCreate && canAssign ? (
|
|
@@ -310,10 +381,14 @@ export default function UsersPage(): ReactElement {
|
|
|
310
381
|
{t('settings.newUser')}
|
|
311
382
|
</Button>
|
|
312
383
|
) : null;
|
|
313
|
-
const filtered =
|
|
384
|
+
const filtered =
|
|
385
|
+
Boolean(params.get('q') || roleScope || role) || status !== 'all';
|
|
314
386
|
return (
|
|
315
387
|
<>
|
|
316
388
|
<PageContainer>
|
|
389
|
+
{returnTo && (
|
|
390
|
+
<BackButton to={returnTo}>{t('settings.backToSource')}</BackButton>
|
|
391
|
+
)}
|
|
317
392
|
<PageHeader
|
|
318
393
|
title={t('settings.users')}
|
|
319
394
|
description={t('settings.usersDescription')}
|
|
@@ -321,6 +396,7 @@ export default function UsersPage(): ReactElement {
|
|
|
321
396
|
/>
|
|
322
397
|
<div className='flex flex-wrap items-center gap-3'>
|
|
323
398
|
<Input
|
|
399
|
+
ref={searchRef}
|
|
324
400
|
className='w-full sm:max-w-xs'
|
|
325
401
|
value={query}
|
|
326
402
|
aria-label={t('settings.userSearch')}
|
|
@@ -348,14 +424,48 @@ export default function UsersPage(): ReactElement {
|
|
|
348
424
|
))}
|
|
349
425
|
</SelectContent>
|
|
350
426
|
</Select>
|
|
427
|
+
<Select
|
|
428
|
+
items={[
|
|
429
|
+
{ value: 'all', label: t('settings.allRoles') },
|
|
430
|
+
...roleChoices,
|
|
431
|
+
]}
|
|
432
|
+
value={selectedRole}
|
|
433
|
+
onValueChange={(value) => {
|
|
434
|
+
if (!value) return;
|
|
435
|
+
setParams(
|
|
436
|
+
(old) => {
|
|
437
|
+
const next = new URLSearchParams(old);
|
|
438
|
+
next.delete('page');
|
|
439
|
+
if (value === 'all') {
|
|
440
|
+
next.delete('roleScope');
|
|
441
|
+
next.delete('role');
|
|
442
|
+
} else {
|
|
443
|
+
const [scope, key] = JSON.parse(value) as [string, string];
|
|
444
|
+
next.set('roleScope', scope);
|
|
445
|
+
next.set('role', key);
|
|
446
|
+
}
|
|
447
|
+
return next;
|
|
448
|
+
},
|
|
449
|
+
{ replace: true },
|
|
450
|
+
);
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
<SelectTrigger aria-label={t('settings.roleFilter')}>
|
|
454
|
+
<SelectValue />
|
|
455
|
+
</SelectTrigger>
|
|
456
|
+
<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'>
|
|
457
|
+
{[
|
|
458
|
+
{ value: 'all', label: t('settings.allRoles') },
|
|
459
|
+
...roleChoices,
|
|
460
|
+
].map((option) => (
|
|
461
|
+
<SelectItem key={option.value} value={option.value}>
|
|
462
|
+
{option.label}
|
|
463
|
+
</SelectItem>
|
|
464
|
+
))}
|
|
465
|
+
</SelectContent>
|
|
466
|
+
</Select>
|
|
351
467
|
{filtered && (
|
|
352
|
-
<Button
|
|
353
|
-
variant='ghost'
|
|
354
|
-
onClick={() => {
|
|
355
|
-
setQuery('');
|
|
356
|
-
setParams({});
|
|
357
|
-
}}
|
|
358
|
-
>
|
|
468
|
+
<Button variant='ghost' onClick={clearFilters}>
|
|
359
469
|
{t('settings.clear')}
|
|
360
470
|
</Button>
|
|
361
471
|
)}
|
|
@@ -365,6 +475,12 @@ export default function UsersPage(): ReactElement {
|
|
|
365
475
|
{t('settings.loading')}
|
|
366
476
|
</span>
|
|
367
477
|
)}
|
|
478
|
+
{Boolean(roleScope) !== Boolean(role) && (
|
|
479
|
+
<p role='alert' className='text-sm text-destructive'>
|
|
480
|
+
{t('settings.roleFilterIncomplete')}
|
|
481
|
+
</p>
|
|
482
|
+
)}
|
|
483
|
+
<SettingsError error={options.error} retry={options.reload} />
|
|
368
484
|
<SettingsError error={request.error} retry={request.reload} />
|
|
369
485
|
{request.loading && !request.data ? (
|
|
370
486
|
<SettingsLoading />
|
|
@@ -377,34 +493,8 @@ export default function UsersPage(): ReactElement {
|
|
|
377
493
|
getRowId={(row) => row.id}
|
|
378
494
|
manualPagination
|
|
379
495
|
rowCount={request.data?.meta.total ?? 0}
|
|
380
|
-
|
|
381
|
-
onPaginationChange={(updater) => {
|
|
382
|
-
const next =
|
|
383
|
-
typeof updater === 'function'
|
|
384
|
-
? updater({ pageIndex: page - 1, pageSize })
|
|
385
|
-
: updater;
|
|
386
|
-
setParams((old) => {
|
|
387
|
-
const result = new URLSearchParams(old);
|
|
388
|
-
result.set('page', String(next.pageIndex + 1));
|
|
389
|
-
result.set('pageSize', String(next.pageSize));
|
|
390
|
-
return result;
|
|
391
|
-
});
|
|
392
|
-
}}
|
|
496
|
+
{...tableState}
|
|
393
497
|
manualSorting
|
|
394
|
-
sortingState={
|
|
395
|
-
params.get('order')
|
|
396
|
-
? [{ id: 'name', desc: params.get('order') === 'desc' }]
|
|
397
|
-
: []
|
|
398
|
-
}
|
|
399
|
-
onSortingChange={(updater) => {
|
|
400
|
-
const previous = params.get('order')
|
|
401
|
-
? [{ id: 'name', desc: params.get('order') === 'desc' }]
|
|
402
|
-
: [];
|
|
403
|
-
const next =
|
|
404
|
-
typeof updater === 'function' ? updater(previous) : updater;
|
|
405
|
-
update('order', next[0] ? (next[0].desc ? 'desc' : 'asc') : '');
|
|
406
|
-
}}
|
|
407
|
-
pageSizeOptions={[20, 50, 100]}
|
|
408
498
|
showSelectedCount={false}
|
|
409
499
|
loading={request.loading}
|
|
410
500
|
/>
|
|
@@ -418,18 +508,16 @@ export default function UsersPage(): ReactElement {
|
|
|
418
508
|
</EmptyTitle>
|
|
419
509
|
<EmptyDescription>
|
|
420
510
|
{t(
|
|
421
|
-
filtered
|
|
511
|
+
filtered
|
|
512
|
+
? 'settings.clearHint'
|
|
513
|
+
: canCreate && canAssign
|
|
514
|
+
? 'settings.usersDescription'
|
|
515
|
+
: 'settings.usersReadOnlyEmpty',
|
|
422
516
|
)}
|
|
423
517
|
</EmptyDescription>
|
|
424
518
|
<EmptyContent>
|
|
425
519
|
{filtered ? (
|
|
426
|
-
<Button
|
|
427
|
-
variant='outline'
|
|
428
|
-
onClick={() => {
|
|
429
|
-
setQuery('');
|
|
430
|
-
setParams({});
|
|
431
|
-
}}
|
|
432
|
-
>
|
|
520
|
+
<Button variant='outline' onClick={clearFilters}>
|
|
433
521
|
{t('settings.clear')}
|
|
434
522
|
</Button>
|
|
435
523
|
) : (
|
package/client/plugins.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import foundation from '@nocobase/app-plugin-foundation/client';
|
|
1
2
|
import defaultAccess from '@nocobase/app-plugin-authz-default-access/client';
|
|
2
3
|
import sharingRules from '@nocobase/app-plugin-authz-sharing-rules/client';
|
|
3
4
|
import restrictionRules from '@nocobase/app-plugin-authz-restriction-rules/client';
|
|
@@ -34,6 +35,7 @@ const clientPlugins: AppClientPlugins = defineClientPlugins([
|
|
|
34
35
|
file(),
|
|
35
36
|
scheduler(),
|
|
36
37
|
mail(),
|
|
38
|
+
foundation({ settingsGroup: true }),
|
|
37
39
|
]);
|
|
38
40
|
|
|
39
41
|
export default clientPlugins;
|
package/config.example.yml
CHANGED
|
@@ -77,15 +77,16 @@ auth:
|
|
|
77
77
|
storeSessionInDatabase: true
|
|
78
78
|
|
|
79
79
|
notification:
|
|
80
|
+
# provider remains a compatibility alias for type; both must match when supplied.
|
|
80
81
|
channels:
|
|
81
82
|
inbox:
|
|
82
|
-
|
|
83
|
+
type: in-app
|
|
83
84
|
|
|
84
85
|
# Uncomment and configure independent email Channels.
|
|
85
86
|
# Values are read as written: ${NAME} is not expanded. To take a secret from the environment instead, map a
|
|
86
87
|
# variable onto its path in env of server/config/notification.ts, such as SMTP_PASSWORD.
|
|
87
88
|
# system-email:
|
|
88
|
-
#
|
|
89
|
+
# type: smtp
|
|
89
90
|
# host: smtp.example.com
|
|
90
91
|
# port: 587
|
|
91
92
|
# auth:
|
|
@@ -93,15 +94,15 @@ notification:
|
|
|
93
94
|
# pass: replace-with-the-smtp-password
|
|
94
95
|
# from: notifications@example.com
|
|
95
96
|
# marketing-email:
|
|
96
|
-
#
|
|
97
|
+
# type: resend
|
|
97
98
|
# apiKey: replace-with-the-resend-api-key
|
|
98
99
|
# from: marketing@example.com
|
|
99
100
|
|
|
100
101
|
# ops-feishu:
|
|
101
|
-
#
|
|
102
|
+
# type: feishu-webhook
|
|
102
103
|
# webhookUrl: https://open.feishu.cn/open-apis/bot/v2/hook/replace-with-the-token
|
|
103
104
|
# ops-dingtalk:
|
|
104
|
-
#
|
|
105
|
+
# type: dingtalk-webhook
|
|
105
106
|
# webhookUrl: https://oapi.dingtalk.com/robot/send?access_token=replace-with-the-token
|
|
106
107
|
|
|
107
108
|
# DB_DIALECT, DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD, DB_SSL and DB_FILENAME override the main
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.76",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.76"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"tar": "^7.5.22",
|
|
73
73
|
"@xyflow/react": "^12.11.5",
|
|
74
74
|
"elkjs": "^0.10.2",
|
|
75
|
-
"@nocobase/app-client": "^4.0.0-beta.
|
|
75
|
+
"@nocobase/app-client": "^4.0.0-beta.1",
|
|
76
76
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
77
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
78
|
-
"@nocobase/app-testing": "0.1.0-beta.
|
|
77
|
+
"@nocobase/app-skills": "0.1.0-beta.44",
|
|
78
|
+
"@nocobase/app-testing": "0.1.0-beta.6",
|
|
79
79
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
80
80
|
"@nocobase/dev-config": "0.1.0-beta.24",
|
|
81
81
|
"@nocobase/drive": "^0.1.0-beta.7",
|
|
@@ -139,28 +139,30 @@
|
|
|
139
139
|
"@oclif/core": "^4.14.0",
|
|
140
140
|
"better-auth": "1.7.5",
|
|
141
141
|
"hono": "^4.13.2",
|
|
142
|
-
"@nocobase/app-plugin-agents": "^1.0.0-beta.
|
|
142
|
+
"@nocobase/app-plugin-agents": "^1.0.0-beta.18",
|
|
143
143
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.18",
|
|
144
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.12",
|
|
144
145
|
"@nocobase/app-plugin-authorization": "^1.0.0-beta.30",
|
|
145
146
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.16",
|
|
146
|
-
"@nocobase/app-plugin-authentication": "^2.0.0-beta.12",
|
|
147
147
|
"@nocobase/app-plugin-file": "^1.0.0-beta.26",
|
|
148
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.1",
|
|
148
149
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.18",
|
|
150
|
+
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.9",
|
|
149
151
|
"@nocobase/app-plugin-mail": "^1.0.0-beta.16",
|
|
150
|
-
"@nocobase/app-plugin-notification": "^
|
|
151
|
-
"@nocobase/app-plugin-notification-in-app": "^
|
|
152
|
-
"@nocobase/app-plugin-notification-providers": "^0.
|
|
152
|
+
"@nocobase/app-plugin-notification": "^2.0.0-beta.0",
|
|
153
|
+
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.0",
|
|
154
|
+
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.15",
|
|
153
155
|
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.19",
|
|
154
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
155
|
-
"@nocobase/app-server": "^3.0.0-beta.
|
|
156
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.9",
|
|
157
|
+
"@nocobase/app-server": "^3.0.0-beta.2",
|
|
156
158
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
157
159
|
"@nocobase/db": "^1.0.0-beta.21",
|
|
158
160
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
159
161
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
160
162
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
163
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.15",
|
|
161
164
|
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.15",
|
|
162
165
|
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.14",
|
|
163
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.15",
|
|
164
166
|
"@nocobase/authorization": "^1.0.0-beta.15",
|
|
165
167
|
"@nocobase/queue": "^0.1.0-beta.9",
|
|
166
168
|
"@nocobase/jobs": "^0.1.0-beta.3",
|
|
@@ -168,7 +170,7 @@
|
|
|
168
170
|
"@nocobase/i18n": "^2.0.0-beta.1",
|
|
169
171
|
"@nocobase/caching": "^0.1.0-beta.3",
|
|
170
172
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
171
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
173
|
+
"@nocobase/app-cli": "^1.0.0-beta.26",
|
|
172
174
|
"@nocobase/realtime": "^0.1.0-beta.3"
|
|
173
175
|
},
|
|
174
176
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
package/server/plugins.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import foundation from '@nocobase/app-plugin-foundation/server';
|
|
1
2
|
import defaultAccess from '@nocobase/app-plugin-authz-default-access/server';
|
|
2
3
|
import sharingRules from '@nocobase/app-plugin-authz-sharing-rules/server';
|
|
3
4
|
import restrictionRules from '@nocobase/app-plugin-authz-restriction-rules/server';
|
|
@@ -34,6 +35,7 @@ const serverPlugins: AppServerPlugins = defineServerPlugins([
|
|
|
34
35
|
file,
|
|
35
36
|
scheduler,
|
|
36
37
|
mail,
|
|
38
|
+
foundation,
|
|
37
39
|
]);
|
|
38
40
|
|
|
39
41
|
export default serverPlugins;
|