@nocobase/app-template-default 1.0.0-beta.75 → 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/README.MD +2 -2
- 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 +21 -5
- 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/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 +37 -1
- package/client/locales/system/settings-zh-CN.ts +33 -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 +44 -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/config.example.yml +6 -5
- package/package.json +11 -11
- package/tests/components/default-access.test.tsx +141 -2
- 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/return-to.test.ts +19 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router';
|
|
3
|
+
import type {
|
|
4
|
+
OnChangeFn,
|
|
5
|
+
PaginationState,
|
|
6
|
+
SortingState,
|
|
7
|
+
} from '@tanstack/react-table';
|
|
8
|
+
|
|
9
|
+
const sizes = [10, 20, 50, 100] as const;
|
|
10
|
+
|
|
11
|
+
/** Keep list position shareable across overlays, refresh and browser history. */
|
|
12
|
+
export function useTableUrlState({
|
|
13
|
+
defaultPageSize = 10,
|
|
14
|
+
pageSizeOptions = sizes,
|
|
15
|
+
sortColumns,
|
|
16
|
+
rowCount,
|
|
17
|
+
}: {
|
|
18
|
+
defaultPageSize?: number;
|
|
19
|
+
pageSizeOptions?: readonly number[];
|
|
20
|
+
sortColumns: readonly string[];
|
|
21
|
+
rowCount?: number;
|
|
22
|
+
}) {
|
|
23
|
+
const [params, setParams] = useSearchParams();
|
|
24
|
+
const requestedPage = Number(params.get('page'));
|
|
25
|
+
const pageIndex =
|
|
26
|
+
Number.isSafeInteger(requestedPage) && requestedPage > 0
|
|
27
|
+
? requestedPage - 1
|
|
28
|
+
: 0;
|
|
29
|
+
const requestedSize = Number(params.get('pageSize'));
|
|
30
|
+
const pageSize = pageSizeOptions.includes(requestedSize)
|
|
31
|
+
? requestedSize
|
|
32
|
+
: defaultPageSize;
|
|
33
|
+
const paginationState = { pageIndex, pageSize };
|
|
34
|
+
const sort = params.get('sort') ?? sortColumns[0];
|
|
35
|
+
const order = params.get('order');
|
|
36
|
+
const sortingState: SortingState =
|
|
37
|
+
sort && sortColumns.includes(sort) && (order === 'asc' || order === 'desc')
|
|
38
|
+
? [{ id: sort, desc: order === 'desc' }]
|
|
39
|
+
: [];
|
|
40
|
+
const onPaginationChange: OnChangeFn<PaginationState> = (updater) => {
|
|
41
|
+
const next =
|
|
42
|
+
typeof updater === 'function' ? updater(paginationState) : updater;
|
|
43
|
+
setParams(
|
|
44
|
+
(old) => {
|
|
45
|
+
const result = new URLSearchParams(old);
|
|
46
|
+
result.set('page', String(next.pageIndex + 1));
|
|
47
|
+
result.set('pageSize', String(next.pageSize));
|
|
48
|
+
return result;
|
|
49
|
+
},
|
|
50
|
+
{ replace: false },
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
const onSortingChange: OnChangeFn<SortingState> = (updater) => {
|
|
54
|
+
const next =
|
|
55
|
+
typeof updater === 'function' ? updater(sortingState) : updater;
|
|
56
|
+
setParams(
|
|
57
|
+
(old) => {
|
|
58
|
+
const result = new URLSearchParams(old);
|
|
59
|
+
result.delete('page');
|
|
60
|
+
if (next[0] && sortColumns.includes(next[0].id)) {
|
|
61
|
+
result.set('sort', next[0].id);
|
|
62
|
+
result.set('order', next[0].desc ? 'desc' : 'asc');
|
|
63
|
+
} else {
|
|
64
|
+
result.delete('sort');
|
|
65
|
+
result.delete('order');
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
{ replace: true },
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
// Only pass a count for the current, successfully loaded query.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (rowCount === undefined) return;
|
|
75
|
+
const lastPage = Math.max(0, Math.ceil(rowCount / pageSize) - 1);
|
|
76
|
+
if (pageIndex <= lastPage) return;
|
|
77
|
+
setParams(
|
|
78
|
+
(old) => {
|
|
79
|
+
const result = new URLSearchParams(old);
|
|
80
|
+
result.set('page', String(lastPage + 1));
|
|
81
|
+
return result;
|
|
82
|
+
},
|
|
83
|
+
{ replace: true },
|
|
84
|
+
);
|
|
85
|
+
}, [rowCount, pageSize, pageIndex, setParams]);
|
|
86
|
+
return {
|
|
87
|
+
paginationState,
|
|
88
|
+
onPaginationChange,
|
|
89
|
+
sortingState,
|
|
90
|
+
onSortingChange,
|
|
91
|
+
pageSizeOptions,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { UsersClient } from '@nocobase/app-plugin-users/client/user-client';
|
|
2
|
+
import {
|
|
3
|
+
setupInspectionFailure,
|
|
4
|
+
type SetupInspection,
|
|
5
|
+
type SetupInspections,
|
|
6
|
+
} from '../components/setup-readiness-state.js';
|
|
7
|
+
import { safeReturnTo } from './return-to.js';
|
|
8
|
+
|
|
9
|
+
function isReadonlyArray<T>(
|
|
10
|
+
value: readonly T[] | undefined,
|
|
11
|
+
): value is readonly T[] {
|
|
12
|
+
return Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RequiredSetupRole {
|
|
16
|
+
readonly scope: string;
|
|
17
|
+
readonly role: string;
|
|
18
|
+
/** A localized business label; never render the role key as a title. */
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
}
|
|
22
|
+
export function requiredRoleSetupId(
|
|
23
|
+
role: Pick<RequiredSetupRole, 'scope' | 'role'>,
|
|
24
|
+
): string {
|
|
25
|
+
return JSON.stringify([role.scope, role.role]);
|
|
26
|
+
}
|
|
27
|
+
export function requiredRoleSetupLink(
|
|
28
|
+
role: Pick<RequiredSetupRole, 'scope' | 'role'>,
|
|
29
|
+
returnTo: string,
|
|
30
|
+
): string {
|
|
31
|
+
const query = new URLSearchParams({
|
|
32
|
+
roleScope: role.scope,
|
|
33
|
+
role: role.role,
|
|
34
|
+
status: 'enabled',
|
|
35
|
+
});
|
|
36
|
+
const source = safeReturnTo(returnTo);
|
|
37
|
+
if (source) query.set('returnTo', source);
|
|
38
|
+
return `/settings/users?${query}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One metadata read and one bounded read per distinct required role. No writes. */
|
|
42
|
+
export async function inspectRequiredRoles(
|
|
43
|
+
client: Pick<UsersClient, 'options' | 'list'>,
|
|
44
|
+
requirements: readonly RequiredSetupRole[],
|
|
45
|
+
): Promise<SetupInspections> {
|
|
46
|
+
const unique = [
|
|
47
|
+
...new Map(
|
|
48
|
+
requirements.map((role) => [requiredRoleSetupId(role), role]),
|
|
49
|
+
).values(),
|
|
50
|
+
];
|
|
51
|
+
if (unique.length === 0) return {};
|
|
52
|
+
const options = await client.options();
|
|
53
|
+
const roleScopes = options?.roleScopes;
|
|
54
|
+
if (!isReadonlyArray(roleScopes))
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
unique.map((role) => [
|
|
57
|
+
requiredRoleSetupId(role),
|
|
58
|
+
{ status: 'unknown', reason: 'invalid' },
|
|
59
|
+
]),
|
|
60
|
+
);
|
|
61
|
+
const entries = await Promise.all(
|
|
62
|
+
unique.map(
|
|
63
|
+
async (requirement): Promise<readonly [string, SetupInspection]> => {
|
|
64
|
+
const id = requiredRoleSetupId(requirement);
|
|
65
|
+
const scope = roleScopes.find(
|
|
66
|
+
(scope) => scope?.key === requirement.scope,
|
|
67
|
+
);
|
|
68
|
+
const role = isReadonlyArray(scope?.options)
|
|
69
|
+
? scope.options.find((role) => role?.value === requirement.role)
|
|
70
|
+
: undefined;
|
|
71
|
+
// Metadata offers direct selectable roles; authenticated defaults apply separately.
|
|
72
|
+
if (!scope || !role)
|
|
73
|
+
return [id, { status: 'unknown', reason: 'invalid' }];
|
|
74
|
+
try {
|
|
75
|
+
const page = await client.list({
|
|
76
|
+
roleScope: requirement.scope,
|
|
77
|
+
role: requirement.role,
|
|
78
|
+
status: 'enabled',
|
|
79
|
+
page: 1,
|
|
80
|
+
pageSize: 1,
|
|
81
|
+
});
|
|
82
|
+
if (!Number.isSafeInteger(page?.total) || page.total < 0)
|
|
83
|
+
return [id, { status: 'unknown', reason: 'invalid' }];
|
|
84
|
+
return [
|
|
85
|
+
id,
|
|
86
|
+
{
|
|
87
|
+
status: page.total > 0 ? 'ready' : 'missing',
|
|
88
|
+
canConfigure: role.assignable !== false,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
return [id, setupInspectionFailure(error)];
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
return Object.fromEntries(entries);
|
|
98
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Accept only an application-relative destination, resolved by the app router's basename. */
|
|
2
|
+
export function safeReturnTo(value: string | null): string | undefined {
|
|
3
|
+
if (
|
|
4
|
+
!value ||
|
|
5
|
+
!value.startsWith('/') ||
|
|
6
|
+
value.startsWith('//') ||
|
|
7
|
+
[...value].some(
|
|
8
|
+
(character) =>
|
|
9
|
+
character === '\\' ||
|
|
10
|
+
character.charCodeAt(0) <= 32 ||
|
|
11
|
+
character.charCodeAt(0) === 127,
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
return;
|
|
15
|
+
const base = 'https://app.invalid';
|
|
16
|
+
const url = new URL(value, base);
|
|
17
|
+
if (url.origin !== base || url.pathname.startsWith('//')) return;
|
|
18
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
19
|
+
}
|
|
@@ -9,6 +9,16 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
|
|
|
9
9
|
*/
|
|
10
10
|
const systemEnUS = {
|
|
11
11
|
flowDiagram: {
|
|
12
|
+
controls: 'Diagram controls',
|
|
13
|
+
fitView: 'Fit to view',
|
|
14
|
+
fullscreen: 'Fullscreen',
|
|
15
|
+
exitFullscreen: 'Exit fullscreen',
|
|
16
|
+
escapeHint: 'Press Escape to exit',
|
|
17
|
+
leavesTo: 'Leaves to',
|
|
18
|
+
arrivesFrom: 'Arrives from',
|
|
19
|
+
otherRoutes: 'Other routes',
|
|
20
|
+
minimap: 'Diagram overview',
|
|
21
|
+
|
|
12
22
|
configure: 'Configure',
|
|
13
23
|
overview: 'Diagram overview',
|
|
14
24
|
actualSize: 'Actual size',
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
const settings = {
|
|
2
|
+
setup: {
|
|
3
|
+
title: 'Feature prerequisites',
|
|
4
|
+
checking: 'Checking current configuration…',
|
|
5
|
+
checkAgain: 'Check again',
|
|
6
|
+
ready: 'The checked prerequisites are ready.',
|
|
7
|
+
missing: 'Review the configuration items below.',
|
|
8
|
+
manageMembers: 'Manage members',
|
|
9
|
+
manageMembersFor: 'Manage {{role}} members',
|
|
10
|
+
unavailable: 'Unable to check right now. Try again.',
|
|
11
|
+
signIn: 'Your session expired. Sign in again to check configuration.',
|
|
12
|
+
forbidden:
|
|
13
|
+
'You do not have access to check this configuration. Contact an administrator.',
|
|
14
|
+
invalid:
|
|
15
|
+
'This prerequisite could not be verified. Ask the application maintainer to check its configuration.',
|
|
16
|
+
},
|
|
17
|
+
roleFilterIncomplete:
|
|
18
|
+
'Choose both a role scope and a role, or clear the filters.',
|
|
19
|
+
backToSource: 'Return to previous page',
|
|
20
|
+
roleFilter: 'Filter by role',
|
|
21
|
+
allRoles: 'All roles',
|
|
22
|
+
|
|
2
23
|
permissionManagement: 'Permission management',
|
|
3
24
|
defaultAccess: {
|
|
4
25
|
noResourcesShort: 'No configurable objects available',
|
|
@@ -83,6 +104,11 @@ const settings = {
|
|
|
83
104
|
|
|
84
105
|
assignedRoles: 'Assigned roles',
|
|
85
106
|
actionSaved: 'Updated access for "{{name}}"',
|
|
107
|
+
userActionSaved: {
|
|
108
|
+
disable: 'Disabled user "{{name}}"',
|
|
109
|
+
enable: 'Enabled user "{{name}}"',
|
|
110
|
+
revokeSessions: 'Revoked active sessions for "{{name}}"',
|
|
111
|
+
},
|
|
86
112
|
|
|
87
113
|
identityConflict: 'This email or username is already in use.',
|
|
88
114
|
signInAgain: 'Sign in again',
|
|
@@ -97,6 +123,7 @@ const settings = {
|
|
|
97
123
|
userSearch: 'Search names, usernames or emails',
|
|
98
124
|
roleSearch: 'Search role names',
|
|
99
125
|
newUser: 'New user',
|
|
126
|
+
editUserFor: 'Edit {{name}}',
|
|
100
127
|
editUser: 'Edit user',
|
|
101
128
|
newRole: 'New role',
|
|
102
129
|
editRole: 'Edit role',
|
|
@@ -117,9 +144,16 @@ const settings = {
|
|
|
117
144
|
noResults: 'No matching results',
|
|
118
145
|
clearHint: 'Try a different search or clear the filters.',
|
|
119
146
|
noUsers: 'No users yet',
|
|
147
|
+
usersReadOnlyEmpty:
|
|
148
|
+
'Contact an administrator to add users or grant the permissions needed to create users and assign roles.',
|
|
149
|
+
rolesReadOnlyEmpty:
|
|
150
|
+
'Contact an administrator to create a role or grant you permission to create roles.',
|
|
120
151
|
noRoles: 'No roles yet',
|
|
121
152
|
create: 'Create',
|
|
122
153
|
edit: 'Edit',
|
|
154
|
+
selectRoles: 'Select roles',
|
|
155
|
+
searchRoles: 'Search roles',
|
|
156
|
+
noRolesFound: 'No roles found.',
|
|
123
157
|
save: 'Save',
|
|
124
158
|
cancel: 'Cancel',
|
|
125
159
|
saving: 'Saving…',
|
|
@@ -149,12 +183,14 @@ const settings = {
|
|
|
149
183
|
discardTitle: 'Discard changes?',
|
|
150
184
|
discardHint: 'Your unsaved changes will be lost.',
|
|
151
185
|
failed: 'Unable to complete the request. Try again.',
|
|
186
|
+
invalidRequest:
|
|
187
|
+
'Some values are invalid. Check your input or filters and try again.',
|
|
152
188
|
retry: 'Retry',
|
|
153
189
|
loading: 'Loading',
|
|
154
190
|
denied: 'You do not have access. Contact an administrator.',
|
|
155
191
|
missing: 'This record does not exist or has been deleted.',
|
|
156
192
|
conflict:
|
|
157
|
-
'
|
|
193
|
+
'The request conflicts with current data. Review the latest record before trying again.',
|
|
158
194
|
sessionEnded: 'Your session has ended. Sign in again.',
|
|
159
195
|
previous: 'Previous',
|
|
160
196
|
next: 'Next',
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
const settings = {
|
|
2
|
+
setup: {
|
|
3
|
+
title: '功能前置配置',
|
|
4
|
+
checking: '正在检查当前配置…',
|
|
5
|
+
checkAgain: '重新检查',
|
|
6
|
+
ready: '已检查的前置配置已就绪。',
|
|
7
|
+
missing: '请检查以下配置项。',
|
|
8
|
+
manageMembers: '管理成员',
|
|
9
|
+
manageMembersFor: '管理{{role}}成员',
|
|
10
|
+
unavailable: '暂时无法检查,请稍后重试。',
|
|
11
|
+
signIn: '登录已失效,请重新登录后检查配置。',
|
|
12
|
+
forbidden: '没有权限检查此配置,请联系管理员。',
|
|
13
|
+
invalid: '无法确认此前置配置,请应用维护者检查其配置声明。',
|
|
14
|
+
},
|
|
15
|
+
roleFilterIncomplete: '请同时指定角色范围与角色,或清除筛选条件。',
|
|
16
|
+
backToSource: '返回原页面',
|
|
17
|
+
roleFilter: '按角色筛选',
|
|
18
|
+
allRoles: '全部角色',
|
|
19
|
+
|
|
2
20
|
permissionManagement: '权限管理',
|
|
3
21
|
defaultAccess: {
|
|
4
22
|
noResourcesShort: '暂无可配置的业务对象',
|
|
@@ -75,6 +93,11 @@ const settings = {
|
|
|
75
93
|
|
|
76
94
|
assignedRoles: '已分配角色',
|
|
77
95
|
actionSaved: '已更新“{{name}}”的访问状态',
|
|
96
|
+
userActionSaved: {
|
|
97
|
+
disable: '已禁用用户“{{name}}”',
|
|
98
|
+
enable: '已启用用户“{{name}}”',
|
|
99
|
+
revokeSessions: '已撤销“{{name}}”的登录会话',
|
|
100
|
+
},
|
|
78
101
|
|
|
79
102
|
identityConflict: '此邮箱或用户名已被使用。',
|
|
80
103
|
signInAgain: '重新登录',
|
|
@@ -89,6 +112,7 @@ const settings = {
|
|
|
89
112
|
userSearch: '搜索姓名、用户名或邮箱',
|
|
90
113
|
roleSearch: '搜索角色名称',
|
|
91
114
|
newUser: '新建用户',
|
|
115
|
+
editUserFor: '编辑 {{name}}',
|
|
92
116
|
editUser: '编辑用户',
|
|
93
117
|
newRole: '新建角色',
|
|
94
118
|
editRole: '编辑角色',
|
|
@@ -109,9 +133,15 @@ const settings = {
|
|
|
109
133
|
noResults: '没有匹配的结果',
|
|
110
134
|
clearHint: '请更换搜索条件或清除筛选。',
|
|
111
135
|
noUsers: '暂无用户',
|
|
136
|
+
usersReadOnlyEmpty:
|
|
137
|
+
'请联系管理员添加用户,或授予创建用户和分配角色所需的权限。',
|
|
138
|
+
rolesReadOnlyEmpty: '请联系管理员创建角色,或授予你创建角色的权限。',
|
|
112
139
|
noRoles: '暂无角色',
|
|
113
140
|
create: '创建',
|
|
114
141
|
edit: '编辑',
|
|
142
|
+
selectRoles: '选择角色',
|
|
143
|
+
searchRoles: '搜索角色',
|
|
144
|
+
noRolesFound: '没有匹配的角色。',
|
|
115
145
|
save: '保存',
|
|
116
146
|
cancel: '取消',
|
|
117
147
|
saving: '正在保存…',
|
|
@@ -139,11 +169,12 @@ const settings = {
|
|
|
139
169
|
discardTitle: '放弃未保存的更改?',
|
|
140
170
|
discardHint: '未保存的更改将丢失。',
|
|
141
171
|
failed: '请求失败,请重试。',
|
|
172
|
+
invalidRequest: '部分内容无效,请检查填写内容或筛选条件后重试。',
|
|
142
173
|
retry: '重试',
|
|
143
174
|
loading: '加载中',
|
|
144
|
-
denied: '
|
|
175
|
+
denied: '没有执行此操作的权限,请联系管理员。',
|
|
145
176
|
missing: '记录不存在或已被删除。',
|
|
146
|
-
conflict: '
|
|
177
|
+
conflict: '此操作与当前数据冲突,请查看最新记录后再试。',
|
|
147
178
|
sessionEnded: '会话已结束,请重新登录。',
|
|
148
179
|
previous: '上一页',
|
|
149
180
|
next: '下一页',
|
|
@@ -6,6 +6,16 @@ import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
|
|
|
6
6
|
/** The Chinese copy of what the template ships; see `./en-US.ts`. */
|
|
7
7
|
const systemZhCN: SystemResource = {
|
|
8
8
|
flowDiagram: {
|
|
9
|
+
controls: '流程图控制',
|
|
10
|
+
fitView: '适应画布',
|
|
11
|
+
fullscreen: '全屏',
|
|
12
|
+
exitFullscreen: '退出全屏',
|
|
13
|
+
escapeHint: '按 Escape 退出',
|
|
14
|
+
leavesTo: '可转到',
|
|
15
|
+
arrivesFrom: '来自',
|
|
16
|
+
otherRoutes: '其他流转',
|
|
17
|
+
minimap: '流程缩略导航',
|
|
18
|
+
|
|
9
19
|
configure: '配置',
|
|
10
20
|
overview: '流程缩略导航',
|
|
11
21
|
actualSize: '实际大小',
|
|
@@ -79,7 +79,10 @@ function DefaultAccessFormBody(): ReactElement {
|
|
|
79
79
|
draft !== undefined && JSON.stringify(draft) !== JSON.stringify(initial);
|
|
80
80
|
const leave = useSettingsLeave(dirty, busy);
|
|
81
81
|
const close = () => {
|
|
82
|
-
void navigate(
|
|
82
|
+
void navigate(
|
|
83
|
+
{ pathname: '/default-access', search: location.search },
|
|
84
|
+
{ replace: true },
|
|
85
|
+
);
|
|
83
86
|
};
|
|
84
87
|
const text = (value: LocalizedText): string =>
|
|
85
88
|
typeof value === 'string'
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RecordLink } from '#components/record-link';
|
|
2
|
+
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
1
3
|
import { useApiClient, useToaster } from '@nocobase/app-client';
|
|
2
4
|
import {
|
|
3
5
|
useCan,
|
|
@@ -119,15 +121,14 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
119
121
|
/>
|
|
120
122
|
),
|
|
121
123
|
cell: ({ row }) => (
|
|
122
|
-
<
|
|
123
|
-
className='text-foreground underline-offset-4 hover:underline'
|
|
124
|
+
<RecordLink
|
|
124
125
|
to={{
|
|
125
126
|
pathname: `${encodeURIComponent(row.original.key)}/edit`,
|
|
126
127
|
search: location.search,
|
|
127
128
|
}}
|
|
128
129
|
>
|
|
129
130
|
{name(row.original)}
|
|
130
|
-
</
|
|
131
|
+
</RecordLink>
|
|
131
132
|
),
|
|
132
133
|
},
|
|
133
134
|
{
|
|
@@ -210,6 +211,29 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
210
211
|
.includes(search.toLocaleLowerCase()),
|
|
211
212
|
)
|
|
212
213
|
.sort((a, b) => name(a).localeCompare(name(b), i18n.language));
|
|
214
|
+
const tableState = useTableUrlState({
|
|
215
|
+
defaultPageSize: 20,
|
|
216
|
+
sortColumns: ['resource'],
|
|
217
|
+
rowCount:
|
|
218
|
+
rules.data &&
|
|
219
|
+
!rules.loading &&
|
|
220
|
+
!rules.error &&
|
|
221
|
+
catalog.data &&
|
|
222
|
+
!catalog.loading &&
|
|
223
|
+
!catalog.error
|
|
224
|
+
? rows.length
|
|
225
|
+
: undefined,
|
|
226
|
+
});
|
|
227
|
+
const clearSearch = () =>
|
|
228
|
+
setParams(
|
|
229
|
+
(old) => {
|
|
230
|
+
const next = new URLSearchParams(old);
|
|
231
|
+
next.delete('q');
|
|
232
|
+
next.delete('page');
|
|
233
|
+
return next;
|
|
234
|
+
},
|
|
235
|
+
{ replace: true },
|
|
236
|
+
);
|
|
213
237
|
const eligible = resources.some(
|
|
214
238
|
(resource) =>
|
|
215
239
|
!rules.data?.data.some(
|
|
@@ -295,13 +319,20 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
295
319
|
placeholder={t('settings.defaultAccess.search')}
|
|
296
320
|
aria-label={t('settings.defaultAccess.search')}
|
|
297
321
|
onChange={(event) =>
|
|
298
|
-
setParams(
|
|
299
|
-
|
|
300
|
-
|
|
322
|
+
setParams(
|
|
323
|
+
(old) => {
|
|
324
|
+
const next = new URLSearchParams(old);
|
|
325
|
+
next.delete('page');
|
|
326
|
+
if (event.target.value) next.set('q', event.target.value);
|
|
327
|
+
else next.delete('q');
|
|
328
|
+
return next;
|
|
329
|
+
},
|
|
330
|
+
{ replace: true },
|
|
331
|
+
)
|
|
301
332
|
}
|
|
302
333
|
/>
|
|
303
334
|
{search && (
|
|
304
|
-
<Button variant='ghost' onClick={
|
|
335
|
+
<Button variant='ghost' onClick={clearSearch}>
|
|
305
336
|
{t('settings.clear')}
|
|
306
337
|
</Button>
|
|
307
338
|
)}
|
|
@@ -329,6 +360,7 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
329
360
|
!catalog.error &&
|
|
330
361
|
(rows.length ? (
|
|
331
362
|
<DataTable
|
|
363
|
+
{...tableState}
|
|
332
364
|
columns={columns}
|
|
333
365
|
data={rows}
|
|
334
366
|
getRowId={(rule) => rule.key}
|
|
@@ -358,7 +390,7 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
358
390
|
</EmptyDescription>
|
|
359
391
|
<EmptyContent>
|
|
360
392
|
{search ? (
|
|
361
|
-
<Button variant='outline' onClick={
|
|
393
|
+
<Button variant='outline' onClick={clearSearch}>
|
|
362
394
|
{t('settings.clear')}
|
|
363
395
|
</Button>
|
|
364
396
|
) : (
|
|
@@ -230,7 +230,7 @@ function RoleEditor(): ReactElement {
|
|
|
230
230
|
parent.reload();
|
|
231
231
|
toaster.show({ type: 'success', title: t('settings.saved', { name }) });
|
|
232
232
|
leave.allow();
|
|
233
|
-
void navigate(closeTo);
|
|
233
|
+
void navigate(closeTo, { replace: true });
|
|
234
234
|
} catch (next) {
|
|
235
235
|
if (
|
|
236
236
|
creating &&
|
|
@@ -10,6 +10,7 @@ import { ShieldCheckIcon, PlusIcon } from 'lucide-react';
|
|
|
10
10
|
import { PermissionManagementPage } from './permission-management-page.js';
|
|
11
11
|
import { Input } from '#components/ui/input';
|
|
12
12
|
import { Button } from '#components/ui/button';
|
|
13
|
+
import { useTableUrlState } from '#hooks/use-table-url-state';
|
|
13
14
|
import { RoleList } from '#components/role-list';
|
|
14
15
|
import {
|
|
15
16
|
Empty,
|
|
@@ -48,6 +49,13 @@ export default function RolesPage(): ReactElement {
|
|
|
48
49
|
i18n.language,
|
|
49
50
|
),
|
|
50
51
|
);
|
|
52
|
+
const tableState = useTableUrlState({
|
|
53
|
+
sortColumns: ['title', 'key'],
|
|
54
|
+
rowCount:
|
|
55
|
+
request.data && !request.loading && !request.error
|
|
56
|
+
? rows.length
|
|
57
|
+
: undefined,
|
|
58
|
+
});
|
|
51
59
|
const create = canCreate ? (
|
|
52
60
|
<Button
|
|
53
61
|
nativeButton={false}
|
|
@@ -70,16 +78,31 @@ export default function RolesPage(): ReactElement {
|
|
|
70
78
|
placeholder={t('settings.roleSearch')}
|
|
71
79
|
aria-label={t('settings.roleSearch')}
|
|
72
80
|
onChange={(event) => {
|
|
73
|
-
setParams(
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
setParams(
|
|
82
|
+
(old) => {
|
|
83
|
+
const next = new URLSearchParams(old);
|
|
84
|
+
next.delete('page');
|
|
85
|
+
if (event.target.value) next.set('q', event.target.value);
|
|
86
|
+
else next.delete('q');
|
|
87
|
+
return next;
|
|
88
|
+
},
|
|
89
|
+
{ replace: true },
|
|
90
|
+
);
|
|
76
91
|
}}
|
|
77
92
|
/>
|
|
78
93
|
{search && (
|
|
79
94
|
<Button
|
|
80
95
|
variant='ghost'
|
|
81
96
|
onClick={() => {
|
|
82
|
-
setParams(
|
|
97
|
+
setParams(
|
|
98
|
+
(old) => {
|
|
99
|
+
const next = new URLSearchParams(old);
|
|
100
|
+
next.delete('q');
|
|
101
|
+
next.delete('page');
|
|
102
|
+
return next;
|
|
103
|
+
},
|
|
104
|
+
{ replace: true },
|
|
105
|
+
);
|
|
83
106
|
}}
|
|
84
107
|
>
|
|
85
108
|
{t('settings.clear')}
|
|
@@ -93,6 +116,7 @@ export default function RolesPage(): ReactElement {
|
|
|
93
116
|
!request.error &&
|
|
94
117
|
(rows.length ? (
|
|
95
118
|
<RoleList
|
|
119
|
+
tableState={tableState}
|
|
96
120
|
ariaLabel={t('settings.roles')}
|
|
97
121
|
roles={rows.map((role) => ({
|
|
98
122
|
key: role.key,
|
|
@@ -120,14 +144,28 @@ export default function RolesPage(): ReactElement {
|
|
|
120
144
|
{t(search ? 'settings.noResults' : 'settings.noRoles')}
|
|
121
145
|
</EmptyTitle>
|
|
122
146
|
<EmptyDescription>
|
|
123
|
-
{t(
|
|
147
|
+
{t(
|
|
148
|
+
search
|
|
149
|
+
? 'settings.clearHint'
|
|
150
|
+
: canCreate
|
|
151
|
+
? 'settings.rolesDescription'
|
|
152
|
+
: 'settings.rolesReadOnlyEmpty',
|
|
153
|
+
)}
|
|
124
154
|
</EmptyDescription>
|
|
125
155
|
<EmptyContent>
|
|
126
156
|
{search ? (
|
|
127
157
|
<Button
|
|
128
158
|
variant='outline'
|
|
129
159
|
onClick={() => {
|
|
130
|
-
setParams(
|
|
160
|
+
setParams(
|
|
161
|
+
(old) => {
|
|
162
|
+
const next = new URLSearchParams(old);
|
|
163
|
+
next.delete('q');
|
|
164
|
+
next.delete('page');
|
|
165
|
+
return next;
|
|
166
|
+
},
|
|
167
|
+
{ replace: true },
|
|
168
|
+
);
|
|
131
169
|
}}
|
|
132
170
|
>
|
|
133
171
|
{t('settings.clear')}
|
|
@@ -32,18 +32,20 @@ export function SettingsError({
|
|
|
32
32
|
<Alert variant='destructive'>
|
|
33
33
|
<AlertDescription>
|
|
34
34
|
{t(
|
|
35
|
-
status ===
|
|
36
|
-
? 'settings.
|
|
37
|
-
: status ===
|
|
38
|
-
? 'settings.
|
|
39
|
-
: status ===
|
|
40
|
-
? 'settings.
|
|
41
|
-
: status ===
|
|
42
|
-
?
|
|
43
|
-
|
|
44
|
-
?
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
status === 400
|
|
36
|
+
? 'settings.invalidRequest'
|
|
37
|
+
: status === 401
|
|
38
|
+
? 'settings.sessionEnded'
|
|
39
|
+
: status === 403
|
|
40
|
+
? 'settings.denied'
|
|
41
|
+
: status === 404
|
|
42
|
+
? 'settings.missing'
|
|
43
|
+
: status === 409
|
|
44
|
+
? error instanceof ApiClientError &&
|
|
45
|
+
error.reason === 'USER_IDENTITY_CONFLICT'
|
|
46
|
+
? 'settings.identityConflict'
|
|
47
|
+
: 'settings.conflict'
|
|
48
|
+
: 'settings.failed',
|
|
47
49
|
)}
|
|
48
50
|
{status === 401 && (
|
|
49
51
|
<Button
|
|
@@ -54,7 +56,7 @@ export function SettingsError({
|
|
|
54
56
|
{t('settings.signInAgain')}
|
|
55
57
|
</Button>
|
|
56
58
|
)}
|
|
57
|
-
{retry && ![401, 403, 404, 409].includes(Number(status)) && (
|
|
59
|
+
{retry && ![400, 401, 403, 404, 409].includes(Number(status)) && (
|
|
58
60
|
<Button variant='outline' size='sm' onClick={retry}>
|
|
59
61
|
{t('settings.retry')}
|
|
60
62
|
</Button>
|