@nocobase/app-template-default 1.0.0-beta.84 → 1.0.0-beta.85
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/client/components/data-table/data-table.tsx +86 -32
- package/client/components/list-feedback.tsx +174 -0
- package/client/components/permission-editor.tsx +40 -33
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
- package/client/hooks/use-list-data.ts +1 -2
- package/client/locales/system/en-US.ts +14 -1
- package/client/locales/system/settings-en-US.ts +0 -10
- package/client/locales/system/settings-zh-CN.ts +0 -8
- package/client/locales/system/zh-CN.ts +14 -1
- package/client/pages/settings/default-access.tsx +50 -72
- package/client/pages/settings/permissions-model.ts +0 -1
- package/client/pages/settings/roles.tsx +63 -97
- package/client/pages/settings/users.tsx +72 -70
- package/client/runtime.ts +0 -4
- package/config.example.yml +24 -0
- package/package.json +19 -17
- package/server/app.ts +0 -2
- package/server/config/auth.ts +2 -0
- package/server/config/concurrency.ts +3 -0
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +1 -13
- package/server/config/notification.ts +1 -1
- package/server/config/queue.ts +1 -7
- package/server/plugins.ts +2 -0
- package/tests/client/data-table.test.tsx +9 -4
- package/tests/client/default-access.test.tsx +43 -5
- package/tests/client/list-feedback.test.tsx +301 -0
- package/tests/client/manual-list-retry.test.tsx +190 -0
- package/tests/client/permission-workspace.test.ts +13 -0
- package/tests/client/settings-continuity.test.tsx +80 -1
- package/tests/client/settings-feedback.test.tsx +7 -15
- package/tests/fixtures/permission-alignment.html +16 -0
- package/tests/fixtures/permission-alignment.tsx +124 -0
- package/tests/playwright/permission-alignment.test.ts +109 -0
- package/tests/server/auth-email-config.test.ts +79 -0
- package/tests/server/concurrency.test.ts +61 -0
- package/tests/server/config.test.ts +34 -14
- package/client/route-overrides.ts +0 -9
- package/client/source-extensions.ts +0 -19
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListState } from '@nocobase/app-client';
|
|
1
2
|
import type { ReactElement, ReactNode } from 'react';
|
|
2
3
|
import type {
|
|
3
4
|
CellContext,
|
|
@@ -17,12 +18,14 @@ export interface WorkflowExecutionRow {
|
|
|
17
18
|
createdAt: ReactNode;
|
|
18
19
|
}
|
|
19
20
|
export interface WorkflowExecutionListProps {
|
|
21
|
+
listState?: ListState;
|
|
22
|
+
onClearFilters?: () => void;
|
|
20
23
|
rows: readonly WorkflowExecutionRow[];
|
|
21
24
|
loaded: boolean;
|
|
22
25
|
loading: boolean;
|
|
23
26
|
filters: ReactNode;
|
|
24
27
|
status?: ReactNode;
|
|
25
|
-
empty
|
|
28
|
+
empty?: ReactNode;
|
|
26
29
|
labels: {
|
|
27
30
|
refresh: string;
|
|
28
31
|
record: string;
|
|
@@ -47,6 +50,8 @@ type ExecutionDateMeta = ColumnMeta<WorkflowExecutionRow, unknown> &
|
|
|
47
50
|
|
|
48
51
|
export function WorkflowExecutionList({
|
|
49
52
|
rows,
|
|
53
|
+
listState,
|
|
54
|
+
onClearFilters,
|
|
50
55
|
loaded,
|
|
51
56
|
loading,
|
|
52
57
|
filters,
|
|
@@ -72,53 +77,61 @@ export function WorkflowExecutionList({
|
|
|
72
77
|
</Button>
|
|
73
78
|
</div>
|
|
74
79
|
{status}
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
80
|
+
{(listState !== undefined || loaded || loading) && (
|
|
81
|
+
<div className='bg-card'>
|
|
82
|
+
<DataTable
|
|
83
|
+
data={rows}
|
|
84
|
+
listState={
|
|
85
|
+
listState ??
|
|
86
|
+
(!loaded ? 'loading' : rows.length ? 'ready' : 'empty')
|
|
87
|
+
}
|
|
88
|
+
onRetry={onRefresh}
|
|
89
|
+
onClearFilters={onClearFilters}
|
|
90
|
+
getRowId={(row) => row.id}
|
|
91
|
+
pagination={false}
|
|
92
|
+
manualSorting
|
|
93
|
+
sortingState={[{ id: 'createdAt', desc: order === 'newest' }]}
|
|
94
|
+
loading={loading}
|
|
95
|
+
emptyMessage={
|
|
96
|
+
loaded ? (
|
|
97
|
+
<Empty>
|
|
98
|
+
<EmptyDescription>{empty}</EmptyDescription>
|
|
99
|
+
</Empty>
|
|
100
|
+
) : (
|
|
101
|
+
''
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
columns={[
|
|
105
|
+
{
|
|
106
|
+
id: 'record',
|
|
107
|
+
header: labels.record,
|
|
108
|
+
enableSorting: false,
|
|
109
|
+
cell: ExecutionRecordCell,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'workflow',
|
|
113
|
+
header: labels.workflow,
|
|
114
|
+
enableSorting: false,
|
|
115
|
+
cell: ExecutionWorkflowCell,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'state',
|
|
119
|
+
header: labels.state,
|
|
120
|
+
enableSorting: false,
|
|
121
|
+
cell: ExecutionStateCell,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'createdAt',
|
|
125
|
+
accessorFn: (row) => row.createdAt,
|
|
126
|
+
enableSorting: true,
|
|
127
|
+
meta: activityMeta,
|
|
128
|
+
header: ExecutionDateHeader,
|
|
129
|
+
cell: ExecutionCreatedAtCell,
|
|
130
|
+
},
|
|
131
|
+
]}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
122
135
|
<div className='flex flex-wrap items-center justify-between gap-3 text-sm'>
|
|
123
136
|
<p>{pagination}</p>
|
|
124
137
|
<div className='flex gap-2'>
|
|
@@ -59,8 +59,7 @@ export function useListData<T>(
|
|
|
59
59
|
path: result.path,
|
|
60
60
|
data: result.path === path || keepPreviousData ? result.data : undefined,
|
|
61
61
|
loading: !active || result.path !== path || result.key !== key,
|
|
62
|
-
error:
|
|
63
|
-
result.path === path && result.key === key ? result.error : undefined,
|
|
62
|
+
error: result.path === path ? result.error : undefined,
|
|
64
63
|
revision,
|
|
65
64
|
reload: useCallback(() => {
|
|
66
65
|
invalidate();
|
|
@@ -8,6 +8,19 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
|
|
|
8
8
|
* upgrade replaces this file, so put the application's own copy in `../en-US.ts` rather than here.
|
|
9
9
|
*/
|
|
10
10
|
const systemEnUS = {
|
|
11
|
+
listFeedback: {
|
|
12
|
+
loading: 'Loading',
|
|
13
|
+
empty: 'No data yet',
|
|
14
|
+
noResults: 'No results',
|
|
15
|
+
forbidden: 'Access denied',
|
|
16
|
+
unauthenticated: 'Session expired',
|
|
17
|
+
failed: 'Unable to load list',
|
|
18
|
+
refreshFailed: 'Refresh failed; showing previous data',
|
|
19
|
+
retry: 'Retry',
|
|
20
|
+
retrying: 'Retrying…',
|
|
21
|
+
clear: 'Clear filters',
|
|
22
|
+
signIn: 'Sign in',
|
|
23
|
+
},
|
|
11
24
|
flowDiagram: {
|
|
12
25
|
controls: 'Diagram controls',
|
|
13
26
|
fitView: 'Fit to view',
|
|
@@ -399,7 +412,7 @@ const systemEnUS = {
|
|
|
399
412
|
back: 'Back',
|
|
400
413
|
},
|
|
401
414
|
dataTable: {
|
|
402
|
-
noResults: 'No results
|
|
415
|
+
noResults: 'No results',
|
|
403
416
|
sortAscending: 'Asc',
|
|
404
417
|
sortDescending: 'Desc',
|
|
405
418
|
hideColumn: 'Hide',
|
|
@@ -50,10 +50,6 @@ const settings = {
|
|
|
50
50
|
records: 'Selected records ({{count}})',
|
|
51
51
|
retained: 'Retain existing configuration',
|
|
52
52
|
search: 'Search business objects or keys',
|
|
53
|
-
empty: 'No default data scopes',
|
|
54
|
-
emptyHint:
|
|
55
|
-
'Create a rule to configure baseline access for a business object.',
|
|
56
|
-
readOnlyEmpty: 'Ask an administrator to configure baseline data access.',
|
|
57
53
|
noResources:
|
|
58
54
|
'Every available business object already has a default rule, or no configurable objects are registered.',
|
|
59
55
|
unsupported:
|
|
@@ -86,8 +82,6 @@ const settings = {
|
|
|
86
82
|
'Manage user information, account security and role assignments. Review each permission separately.',
|
|
87
83
|
businessOperationsHint:
|
|
88
84
|
'Choose the operations this role can perform and the data each operation can use.',
|
|
89
|
-
pageAccessHint:
|
|
90
|
-
'Choose the business pages this role can open. All system settings, including user management and business configuration, are controlled under Configuration permissions. Data and business actions require separate permissions.',
|
|
91
85
|
roleAssignment: 'Role assignment',
|
|
92
86
|
accountSecurity: 'Account security',
|
|
93
87
|
userInformation: 'User information',
|
|
@@ -145,10 +139,6 @@ const settings = {
|
|
|
145
139
|
noResults: 'No matching results',
|
|
146
140
|
clearHint: 'Try a different search or clear the filters.',
|
|
147
141
|
noUsers: 'No users yet',
|
|
148
|
-
usersReadOnlyEmpty:
|
|
149
|
-
'Contact an administrator to add users or grant the permissions needed to create users and assign roles.',
|
|
150
|
-
rolesReadOnlyEmpty:
|
|
151
|
-
'Contact an administrator to create a role or grant you permission to create roles.',
|
|
152
142
|
noRoleAccess:
|
|
153
143
|
'This user can still sign in and use account basics. No business access is granted by roles.',
|
|
154
144
|
noAssignedRoles: 'No assigned roles',
|
|
@@ -44,9 +44,6 @@ const settings = {
|
|
|
44
44
|
records: '指定记录({{count}} 条)',
|
|
45
45
|
retained: '保留现有配置',
|
|
46
46
|
search: '搜索业务对象或标识',
|
|
47
|
-
empty: '暂无默认数据范围',
|
|
48
|
-
emptyHint: '新建规则,为业务对象配置基础数据访问范围。',
|
|
49
|
-
readOnlyEmpty: '请联系管理员配置基础数据访问范围。',
|
|
50
47
|
noResources: '可用业务对象均已配置默认规则,或当前没有可配置的业务对象。',
|
|
51
48
|
unsupported:
|
|
52
49
|
'此规则包含当前插件目录中不存在的配置,已保留原值并以只读方式展示。',
|
|
@@ -75,8 +72,6 @@ const settings = {
|
|
|
75
72
|
'管理用户资料、账号安全和角色分配,请逐项确认需要授予的权限。',
|
|
76
73
|
businessOperationsHint:
|
|
77
74
|
'设置此角色可以执行的业务操作,以及每项操作可使用的数据范围。',
|
|
78
|
-
pageAccessHint:
|
|
79
|
-
'设置此角色可访问的业务页面。系统设置(包括用户管理、业务配置项)统一在“配置项权限”中控制。页面内的数据和业务操作仍需单独授权。',
|
|
80
75
|
roleAssignment: '角色分配',
|
|
81
76
|
accountSecurity: '账号安全',
|
|
82
77
|
userInformation: '用户资料',
|
|
@@ -134,9 +129,6 @@ const settings = {
|
|
|
134
129
|
noResults: '没有匹配的结果',
|
|
135
130
|
clearHint: '请更换搜索条件或清除筛选。',
|
|
136
131
|
noUsers: '暂无用户',
|
|
137
|
-
usersReadOnlyEmpty:
|
|
138
|
-
'请联系管理员添加用户,或授予创建用户和分配角色所需的权限。',
|
|
139
|
-
rolesReadOnlyEmpty: '请联系管理员创建角色,或授予你创建角色的权限。',
|
|
140
132
|
noRoleAccess: '此用户仍可登录并使用账号基础功能,角色未授予业务访问。',
|
|
141
133
|
noAssignedRoles: '未分配角色',
|
|
142
134
|
noRoles: '暂无角色',
|
|
@@ -5,6 +5,19 @@ import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
|
|
|
5
5
|
|
|
6
6
|
/** The Chinese copy of what the template ships; see `./en-US.ts`. */
|
|
7
7
|
const systemZhCN: SystemResource = {
|
|
8
|
+
listFeedback: {
|
|
9
|
+
loading: '加载中',
|
|
10
|
+
empty: '暂无数据',
|
|
11
|
+
noResults: '无结果',
|
|
12
|
+
forbidden: '无权访问',
|
|
13
|
+
unauthenticated: '会话已过期',
|
|
14
|
+
failed: '列表加载失败',
|
|
15
|
+
refreshFailed: '刷新失败,仍显示旧数据',
|
|
16
|
+
retry: '重试',
|
|
17
|
+
retrying: '正在重试…',
|
|
18
|
+
clear: '清空筛选',
|
|
19
|
+
signIn: '登录',
|
|
20
|
+
},
|
|
8
21
|
flowDiagram: {
|
|
9
22
|
controls: '流程图控制',
|
|
10
23
|
fitView: '适应画布',
|
|
@@ -374,7 +387,7 @@ const systemZhCN: SystemResource = {
|
|
|
374
387
|
back: '返回',
|
|
375
388
|
},
|
|
376
389
|
dataTable: {
|
|
377
|
-
noResults: '
|
|
390
|
+
noResults: '无结果',
|
|
378
391
|
sortAscending: '升序',
|
|
379
392
|
sortDescending: '降序',
|
|
380
393
|
hideColumn: '隐藏',
|
|
@@ -3,6 +3,7 @@ import { RecordLink } from '#components/record-link';
|
|
|
3
3
|
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
4
4
|
import {
|
|
5
5
|
ApiClientError,
|
|
6
|
+
resolveListState,
|
|
6
7
|
useApiClient,
|
|
7
8
|
useSubmission,
|
|
8
9
|
useToaster,
|
|
@@ -15,12 +16,13 @@ import {
|
|
|
15
16
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
16
17
|
import { useRef, useState, type ReactElement } from 'react';
|
|
17
18
|
import { Link, Outlet, useLocation } from 'react-router';
|
|
18
|
-
import {
|
|
19
|
+
import { MoreHorizontalIcon, PlusIcon } from 'lucide-react';
|
|
19
20
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
20
21
|
import { Alert, AlertDescription } from '#components/ui/alert';
|
|
21
22
|
import { PermissionManagementPage } from './permission-management-page.js';
|
|
22
23
|
import { DataTable, DataTableColumnHeader } from '#components/data-table';
|
|
23
24
|
import { Button } from '#components/ui/button';
|
|
25
|
+
import { Spinner } from '#components/ui/spinner';
|
|
24
26
|
import { Input } from '#components/ui/input';
|
|
25
27
|
import {
|
|
26
28
|
DropdownMenu,
|
|
@@ -29,14 +31,7 @@ import {
|
|
|
29
31
|
DropdownMenuSeparator,
|
|
30
32
|
DropdownMenuTrigger,
|
|
31
33
|
} from '#components/ui/dropdown-menu';
|
|
32
|
-
import {
|
|
33
|
-
Empty,
|
|
34
|
-
EmptyMedia,
|
|
35
|
-
EmptyTitle,
|
|
36
|
-
EmptyDescription,
|
|
37
|
-
EmptyContent,
|
|
38
|
-
} from '#components/ui/empty';
|
|
39
|
-
import { ConfirmAction, SettingsError, SettingsLoading } from './shared.js';
|
|
34
|
+
import { ConfirmAction, SettingsError } from './shared.js';
|
|
40
35
|
import { useSettingsData } from './hooks.js';
|
|
41
36
|
import {
|
|
42
37
|
actionKey,
|
|
@@ -61,10 +56,11 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
61
56
|
const catalog = useSettingsData<{ data: AuthorizationOptionsResponse }>(
|
|
62
57
|
'authorization/defaultAccess/options',
|
|
63
58
|
);
|
|
64
|
-
const
|
|
59
|
+
const createPermission = useCan({
|
|
65
60
|
resource: { type: 'settings', id: 'authorization.default-access' },
|
|
66
61
|
action: 'create',
|
|
67
|
-
})
|
|
62
|
+
});
|
|
63
|
+
const canCreate = createPermission.can && !createPermission.isPending;
|
|
68
64
|
const canDelete = useCan({
|
|
69
65
|
resource: { type: 'settings', id: 'authorization.default-access' },
|
|
70
66
|
action: 'delete',
|
|
@@ -233,6 +229,30 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
233
229
|
? rows.length
|
|
234
230
|
: undefined,
|
|
235
231
|
});
|
|
232
|
+
const listError =
|
|
233
|
+
[rules.error, catalog.error].find(
|
|
234
|
+
(error) =>
|
|
235
|
+
error instanceof ApiClientError && [401, 403].includes(error.status),
|
|
236
|
+
) ??
|
|
237
|
+
tableState.error ??
|
|
238
|
+
rules.error ??
|
|
239
|
+
catalog.error;
|
|
240
|
+
const listData = rules.data && catalog.data ? rows : undefined;
|
|
241
|
+
const listState = resolveListState(
|
|
242
|
+
{
|
|
243
|
+
data: listData,
|
|
244
|
+
error: listError,
|
|
245
|
+
isPending: !listData && (rules.loading || catalog.loading),
|
|
246
|
+
},
|
|
247
|
+
rows.length,
|
|
248
|
+
Boolean(search.trim()),
|
|
249
|
+
);
|
|
250
|
+
const canShowCreate =
|
|
251
|
+
canCreate &&
|
|
252
|
+
(listState === 'empty' ||
|
|
253
|
+
listState === 'ready' ||
|
|
254
|
+
listState === 'no-results' ||
|
|
255
|
+
listState === 'refresh-failed');
|
|
236
256
|
const clearSearch = () =>
|
|
237
257
|
setParams(
|
|
238
258
|
(old) => {
|
|
@@ -331,9 +351,7 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
331
351
|
<>
|
|
332
352
|
<PermissionManagementPage
|
|
333
353
|
tab='default-access'
|
|
334
|
-
actions={
|
|
335
|
-
rules.data && (rules.data.data.length || search) ? create : undefined
|
|
336
|
-
}
|
|
354
|
+
actions={canShowCreate ? create : undefined}
|
|
337
355
|
>
|
|
338
356
|
<div className='flex flex-wrap items-center gap-2'>
|
|
339
357
|
<Input
|
|
@@ -355,11 +373,16 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
355
373
|
)
|
|
356
374
|
}
|
|
357
375
|
/>
|
|
358
|
-
{search && (
|
|
376
|
+
{search && listState !== 'no-results' && (
|
|
359
377
|
<Button variant='ghost' onClick={clearSearch}>
|
|
360
378
|
{t('settings.clear')}
|
|
361
379
|
</Button>
|
|
362
380
|
)}
|
|
381
|
+
{active &&
|
|
382
|
+
(rules.loading || catalog.loading) &&
|
|
383
|
+
(rules.data || catalog.data) ? (
|
|
384
|
+
<Spinner className='text-muted-foreground' />
|
|
385
|
+
) : null}
|
|
363
386
|
</div>
|
|
364
387
|
{conflict && (
|
|
365
388
|
<Alert variant='destructive'>
|
|
@@ -368,67 +391,22 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
368
391
|
</AlertDescription>
|
|
369
392
|
</Alert>
|
|
370
393
|
)}
|
|
371
|
-
<SettingsError
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
394
|
+
<SettingsError error={!removing && error} />
|
|
395
|
+
<DataTable
|
|
396
|
+
{...tableState}
|
|
397
|
+
columns={columns}
|
|
398
|
+
data={rows}
|
|
399
|
+
getRowId={(rule) => rule.key}
|
|
400
|
+
showSelectedCount={false}
|
|
401
|
+
pageSize={20}
|
|
402
|
+
loading={rules.loading || catalog.loading}
|
|
403
|
+
listState={listState}
|
|
404
|
+
onRetry={() => {
|
|
378
405
|
rules.reload();
|
|
379
406
|
catalog.reload();
|
|
380
|
-
setError(undefined);
|
|
381
407
|
}}
|
|
408
|
+
onClearFilters={clearSearch}
|
|
382
409
|
/>
|
|
383
|
-
{(rules.loading && !rules.data) ||
|
|
384
|
-
(catalog.loading && !catalog.data) ? (
|
|
385
|
-
<SettingsLoading />
|
|
386
|
-
) : (
|
|
387
|
-
!tableState.error &&
|
|
388
|
-
!rules.error &&
|
|
389
|
-
!catalog.error &&
|
|
390
|
-
(rows.length ? (
|
|
391
|
-
<DataTable
|
|
392
|
-
{...tableState}
|
|
393
|
-
columns={columns}
|
|
394
|
-
data={rows}
|
|
395
|
-
getRowId={(rule) => rule.key}
|
|
396
|
-
showSelectedCount={false}
|
|
397
|
-
pageSize={20}
|
|
398
|
-
/>
|
|
399
|
-
) : (
|
|
400
|
-
<Empty className='min-h-48 border border-dashed'>
|
|
401
|
-
<EmptyMedia variant='icon'>
|
|
402
|
-
<ListFilterIcon />
|
|
403
|
-
</EmptyMedia>
|
|
404
|
-
<EmptyTitle>
|
|
405
|
-
{t(
|
|
406
|
-
search
|
|
407
|
-
? 'settings.noResults'
|
|
408
|
-
: 'settings.defaultAccess.empty',
|
|
409
|
-
)}
|
|
410
|
-
</EmptyTitle>
|
|
411
|
-
<EmptyDescription>
|
|
412
|
-
{t(
|
|
413
|
-
search
|
|
414
|
-
? 'settings.clearHint'
|
|
415
|
-
: canCreate
|
|
416
|
-
? 'settings.defaultAccess.emptyHint'
|
|
417
|
-
: 'settings.defaultAccess.readOnlyEmpty',
|
|
418
|
-
)}
|
|
419
|
-
</EmptyDescription>
|
|
420
|
-
<EmptyContent>
|
|
421
|
-
{search ? (
|
|
422
|
-
<Button variant='outline' onClick={clearSearch}>
|
|
423
|
-
{t('settings.clear')}
|
|
424
|
-
</Button>
|
|
425
|
-
) : (
|
|
426
|
-
create
|
|
427
|
-
)}
|
|
428
|
-
</EmptyContent>
|
|
429
|
-
</Empty>
|
|
430
|
-
))
|
|
431
|
-
)}
|
|
432
410
|
<ConfirmAction
|
|
433
411
|
error={<SettingsError error={error} />}
|
|
434
412
|
open={Boolean(removing)}
|
|
@@ -5,22 +5,17 @@ import type {
|
|
|
5
5
|
PermissionSet,
|
|
6
6
|
LocalizedText,
|
|
7
7
|
} from '@nocobase/app-plugin-authorization/client';
|
|
8
|
+
import { resolveListState } from '@nocobase/app-client';
|
|
8
9
|
import { type ReactElement } from 'react';
|
|
9
10
|
import { Link, Outlet, useLocation } from 'react-router';
|
|
10
|
-
import {
|
|
11
|
+
import { PlusIcon } from 'lucide-react';
|
|
11
12
|
import { PermissionManagementPage } from './permission-management-page.js';
|
|
12
13
|
import { Input } from '#components/ui/input';
|
|
13
14
|
import { Button } from '#components/ui/button';
|
|
15
|
+
import { Spinner } from '#components/ui/spinner';
|
|
14
16
|
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
15
17
|
import { RoleList } from '#components/role-list';
|
|
16
|
-
import {
|
|
17
|
-
Empty,
|
|
18
|
-
EmptyTitle,
|
|
19
|
-
EmptyDescription,
|
|
20
|
-
EmptyContent,
|
|
21
|
-
EmptyMedia,
|
|
22
|
-
} from '#components/ui/empty';
|
|
23
|
-
import { SettingsError, SettingsLoading } from './shared.js';
|
|
18
|
+
import { ListFeedback, ListFeedbackActions } from '#components/list-feedback';
|
|
24
19
|
|
|
25
20
|
export default function RolesPage(): ReactElement {
|
|
26
21
|
const { t, i18n } = useTranslation();
|
|
@@ -60,6 +55,31 @@ export default function RolesPage(): ReactElement {
|
|
|
60
55
|
? rows.length
|
|
61
56
|
: undefined,
|
|
62
57
|
});
|
|
58
|
+
const listState = resolveListState(
|
|
59
|
+
{
|
|
60
|
+
data: request.data,
|
|
61
|
+
error: tableState.error ?? request.error,
|
|
62
|
+
isPending: request.loading && !request.data,
|
|
63
|
+
},
|
|
64
|
+
rows.length,
|
|
65
|
+
Boolean(search.trim()),
|
|
66
|
+
);
|
|
67
|
+
const canShowCreate =
|
|
68
|
+
canCreate &&
|
|
69
|
+
(listState === 'empty' ||
|
|
70
|
+
listState === 'ready' ||
|
|
71
|
+
listState === 'no-results' ||
|
|
72
|
+
listState === 'refresh-failed');
|
|
73
|
+
const clearSearch = () =>
|
|
74
|
+
setParams(
|
|
75
|
+
(old) => {
|
|
76
|
+
const next = new URLSearchParams(old);
|
|
77
|
+
next.delete('q');
|
|
78
|
+
next.delete('page');
|
|
79
|
+
return next;
|
|
80
|
+
},
|
|
81
|
+
{ replace: true },
|
|
82
|
+
);
|
|
63
83
|
const create = canCreate ? (
|
|
64
84
|
<Button
|
|
65
85
|
nativeButton={false}
|
|
@@ -73,7 +93,7 @@ export default function RolesPage(): ReactElement {
|
|
|
73
93
|
<>
|
|
74
94
|
<PermissionManagementPage
|
|
75
95
|
tab='roles'
|
|
76
|
-
actions={
|
|
96
|
+
actions={canShowCreate ? create : undefined}
|
|
77
97
|
>
|
|
78
98
|
<div className='flex items-center gap-3'>
|
|
79
99
|
<Input
|
|
@@ -94,97 +114,43 @@ export default function RolesPage(): ReactElement {
|
|
|
94
114
|
);
|
|
95
115
|
}}
|
|
96
116
|
/>
|
|
97
|
-
{search && (
|
|
98
|
-
<Button
|
|
99
|
-
variant='ghost'
|
|
100
|
-
onClick={() => {
|
|
101
|
-
setParams(
|
|
102
|
-
(old) => {
|
|
103
|
-
const next = new URLSearchParams(old);
|
|
104
|
-
next.delete('q');
|
|
105
|
-
next.delete('page');
|
|
106
|
-
return next;
|
|
107
|
-
},
|
|
108
|
-
{ replace: true },
|
|
109
|
-
);
|
|
110
|
-
}}
|
|
111
|
-
>
|
|
117
|
+
{search && listState !== 'no-results' && (
|
|
118
|
+
<Button variant='ghost' onClick={clearSearch}>
|
|
112
119
|
{t('settings.clear')}
|
|
113
120
|
</Button>
|
|
114
121
|
)}
|
|
122
|
+
<ListFeedbackActions
|
|
123
|
+
state={listState}
|
|
124
|
+
onClear={clearSearch}
|
|
125
|
+
onRetry={request.reload}
|
|
126
|
+
busy={request.loading}
|
|
127
|
+
/>
|
|
128
|
+
{active && request.loading && request.data ? (
|
|
129
|
+
<Spinner className='text-muted-foreground' />
|
|
130
|
+
) : null}
|
|
115
131
|
</div>
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
labels={{
|
|
139
|
-
name: t('settings.roleName'),
|
|
140
|
-
key: t('settings.key'),
|
|
141
|
-
type: t('settings.roleType'),
|
|
142
|
-
protected: t('settings.protected'),
|
|
143
|
-
custom: t('settings.custom'),
|
|
144
|
-
}}
|
|
145
|
-
/>
|
|
146
|
-
) : (
|
|
147
|
-
<Empty className='min-h-48 border border-dashed'>
|
|
148
|
-
<EmptyMedia variant='icon'>
|
|
149
|
-
<ShieldCheckIcon />
|
|
150
|
-
</EmptyMedia>
|
|
151
|
-
<EmptyTitle>
|
|
152
|
-
{t(search ? 'settings.noResults' : 'settings.noRoles')}
|
|
153
|
-
</EmptyTitle>
|
|
154
|
-
<EmptyDescription>
|
|
155
|
-
{t(
|
|
156
|
-
search
|
|
157
|
-
? 'settings.clearHint'
|
|
158
|
-
: canCreate
|
|
159
|
-
? 'settings.rolesDescription'
|
|
160
|
-
: 'settings.rolesReadOnlyEmpty',
|
|
161
|
-
)}
|
|
162
|
-
</EmptyDescription>
|
|
163
|
-
<EmptyContent>
|
|
164
|
-
{search ? (
|
|
165
|
-
<Button
|
|
166
|
-
variant='outline'
|
|
167
|
-
onClick={() => {
|
|
168
|
-
setParams(
|
|
169
|
-
(old) => {
|
|
170
|
-
const next = new URLSearchParams(old);
|
|
171
|
-
next.delete('q');
|
|
172
|
-
next.delete('page');
|
|
173
|
-
return next;
|
|
174
|
-
},
|
|
175
|
-
{ replace: true },
|
|
176
|
-
);
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
179
|
-
{t('settings.clear')}
|
|
180
|
-
</Button>
|
|
181
|
-
) : (
|
|
182
|
-
create
|
|
183
|
-
)}
|
|
184
|
-
</EmptyContent>
|
|
185
|
-
</Empty>
|
|
186
|
-
))
|
|
187
|
-
)}
|
|
132
|
+
<ListFeedback state={listState}>
|
|
133
|
+
<RoleList
|
|
134
|
+
tableState={tableState}
|
|
135
|
+
ariaLabel={t('settings.roles')}
|
|
136
|
+
roles={rows.map((role) => ({
|
|
137
|
+
key: role.key,
|
|
138
|
+
title: text(role.title ?? role.key),
|
|
139
|
+
to: {
|
|
140
|
+
pathname: `${encodeURIComponent(role.key)}/edit`,
|
|
141
|
+
search: location.search,
|
|
142
|
+
},
|
|
143
|
+
protected: Boolean(role.protection),
|
|
144
|
+
}))}
|
|
145
|
+
labels={{
|
|
146
|
+
name: t('settings.roleName'),
|
|
147
|
+
key: t('settings.key'),
|
|
148
|
+
type: t('settings.roleType'),
|
|
149
|
+
protected: t('settings.protected'),
|
|
150
|
+
custom: t('settings.custom'),
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</ListFeedback>
|
|
188
154
|
</PermissionManagementPage>
|
|
189
155
|
<Outlet context={{ reload: request.reload }} />
|
|
190
156
|
</>
|