@nocobase/app-template-default 1.0.0-beta.75 → 1.0.0-beta.77
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/default-access-editor.tsx +79 -52
- 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/long-text.tsx +200 -0
- package/client/components/permission-editor.tsx +534 -341
- 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 +51 -4
- package/client/locales/system/settings-zh-CN.ts +46 -5
- package/client/locales/system/zh-CN.ts +10 -0
- package/client/pages/settings/default-access-form.tsx +14 -2
- package/client/pages/settings/default-access-model.ts +6 -2
- package/client/pages/settings/default-access.tsx +40 -8
- package/client/pages/settings/page-permissions.ts +166 -1
- package/client/pages/settings/permission-filters.tsx +9 -2
- package/client/pages/settings/permission-workspace.ts +330 -0
- package/client/pages/settings/permissions-model.ts +62 -53
- package/client/pages/settings/role-form.tsx +31 -95
- 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 +17 -17
- package/server/config/app.ts +1 -1
- package/server/config/session.ts +7 -5
- package/tests/components/default-access-editor.test.tsx +130 -0
- package/tests/components/default-access.test.tsx +141 -2
- package/tests/components/permission-editor.test.tsx +613 -572
- package/tests/components/permission-filter-binding.test.tsx +20 -0
- package/tests/components/settings-continuity.test.tsx +426 -0
- package/tests/components/settings-feedback.test.tsx +304 -0
- package/tests/components/settings.test.tsx +34 -9
- 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/components/user-permissions.test.tsx +47 -13
- package/tests/logic/config.test.ts +3 -1
- package/tests/logic/page-permission-navigation.test.ts +394 -0
- package/tests/logic/permission-workspace.test.ts +570 -0
- package/tests/logic/return-to.test.ts +19 -0
- package/tests/logic/session-isolation.test.ts +250 -0
- package/tsconfig.json +1 -0
|
@@ -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',
|
|
@@ -57,7 +78,7 @@ const settings = {
|
|
|
57
78
|
scopeExplanation:
|
|
58
79
|
'Each range applies to its corresponding data. The operation must also pass its authorization checks.',
|
|
59
80
|
noAdditionalScopeHint:
|
|
60
|
-
'This
|
|
81
|
+
'This role adds no records. Effective access also depends on other roles and default, sharing and restriction rules.',
|
|
61
82
|
systemToolsHint:
|
|
62
83
|
'Manage system configuration and tools. Review the capabilities each action includes before granting access.',
|
|
63
84
|
userAccountsHint:
|
|
@@ -65,7 +86,7 @@ const settings = {
|
|
|
65
86
|
businessOperationsHint:
|
|
66
87
|
'Choose the operations this role can perform and the data each operation can use.',
|
|
67
88
|
pageAccessHint:
|
|
68
|
-
'
|
|
89
|
+
'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.',
|
|
69
90
|
roleAssignment: 'Role assignment',
|
|
70
91
|
accountSecurity: 'Account security',
|
|
71
92
|
userInformation: 'User information',
|
|
@@ -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,28 +183,41 @@ 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',
|
|
161
197
|
total_one: '{{count}} user',
|
|
162
198
|
total_other: '{{count}} users',
|
|
163
199
|
access: 'Access',
|
|
200
|
+
expandPermissionGroup: 'Expand {{name}}',
|
|
201
|
+
collapsePermissionGroup: 'Collapse {{name}}',
|
|
202
|
+
pagesOutsideNavigation: 'Pages outside navigation',
|
|
203
|
+
configurationPermissions: 'Configuration permissions',
|
|
204
|
+
configurationPermissionsHint:
|
|
205
|
+
'Control access and actions for system settings, user management and business configuration. Only actual application features are shown.',
|
|
164
206
|
permissionSearch: 'Search categories, groups or permissions',
|
|
165
207
|
enabledOnly: 'Enabled only',
|
|
166
208
|
noPermissions: 'No permissions to configure',
|
|
209
|
+
permissionEmptyHint:
|
|
210
|
+
'Ask the application developer to expose the permissions this role can configure.',
|
|
211
|
+
permissionReadOnlyHint: 'These permissions are read-only.',
|
|
212
|
+
permissionManagedHint: 'This permission is managed by the application.',
|
|
213
|
+
permissionSaveHint: 'Permission changes take effect after saving.',
|
|
167
214
|
permissionCategories: 'Permission categories',
|
|
168
215
|
selectAllPermissions: 'All',
|
|
169
216
|
dataRanges: 'Data ranges ({{count}})',
|
|
170
217
|
selectVisiblePermissions: 'Select visible permissions in {{name}}',
|
|
171
218
|
permissionSummary: '{{enabled}} of {{total}} enabled',
|
|
172
219
|
preservedRecordScope: 'Keep existing configuration',
|
|
173
|
-
noRecordScope: '
|
|
220
|
+
noRecordScope: 'Use default rules',
|
|
174
221
|
permissionFilter: {
|
|
175
222
|
limit: 'Up to 20 conditions',
|
|
176
223
|
configure: 'Conditions',
|
|
@@ -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: '暂无可配置的业务对象',
|
|
@@ -50,14 +68,14 @@ const settings = {
|
|
|
50
68
|
scopeExplanation:
|
|
51
69
|
'每项范围分别约束对应数据,实际操作还需通过相应的权限检查。',
|
|
52
70
|
noAdditionalScopeHint:
|
|
53
|
-
'
|
|
71
|
+
'此角色不增加记录范围;实际访问还取决于其他角色及默认、共享和限制规则。',
|
|
54
72
|
systemToolsHint: '管理系统配置与工具,授权前请确认每项操作包含的能力。',
|
|
55
73
|
userAccountsHint:
|
|
56
74
|
'管理用户资料、账号安全和角色分配,请逐项确认需要授予的权限。',
|
|
57
75
|
businessOperationsHint:
|
|
58
76
|
'设置此角色可以执行的业务操作,以及每项操作可使用的数据范围。',
|
|
59
77
|
pageAccessHint:
|
|
60
|
-
'
|
|
78
|
+
'设置此角色可访问的业务页面。系统设置(包括用户管理、业务配置项)统一在“配置项权限”中控制。页面内的数据和业务操作仍需单独授权。',
|
|
61
79
|
roleAssignment: '角色分配',
|
|
62
80
|
accountSecurity: '账号安全',
|
|
63
81
|
userInformation: '用户资料',
|
|
@@ -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,26 +169,37 @@ 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: '下一页',
|
|
150
181
|
total_one: '共 {{count}} 位用户',
|
|
151
182
|
total_other: '共 {{count}} 位用户',
|
|
152
183
|
access: '访问',
|
|
184
|
+
expandPermissionGroup: '展开“{{name}}”',
|
|
185
|
+
collapsePermissionGroup: '收起“{{name}}”',
|
|
186
|
+
pagesOutsideNavigation: '未显示在菜单中的页面',
|
|
187
|
+
configurationPermissions: '配置项权限',
|
|
188
|
+
configurationPermissionsHint:
|
|
189
|
+
'统一设置系统设置、用户管理和业务配置项的访问与操作权限,仅显示应用实际开放的功能。',
|
|
153
190
|
permissionSearch: '搜索分类、分组或权限',
|
|
154
191
|
enabledOnly: '仅显示已授权',
|
|
155
192
|
noPermissions: '暂无可配置权限',
|
|
193
|
+
permissionEmptyHint: '请联系应用开发者,开放此角色需要配置的权限。',
|
|
194
|
+
permissionReadOnlyHint: '当前权限配置为只读。',
|
|
195
|
+
permissionManagedHint: '此权限由应用管理。',
|
|
196
|
+
permissionSaveHint: '权限修改保存后生效。',
|
|
156
197
|
permissionCategories: '权限分类',
|
|
157
198
|
selectAllPermissions: '全选',
|
|
158
199
|
dataRanges: '数据范围({{count}})',
|
|
159
200
|
selectVisiblePermissions: '选择“{{name}}”中显示的权限',
|
|
160
201
|
permissionSummary: '已授权 {{enabled}} / {{total}}',
|
|
161
|
-
noRecordScope: '
|
|
202
|
+
noRecordScope: '按默认规则',
|
|
162
203
|
preservedRecordScope: '保留现有配置',
|
|
163
204
|
permissionFilter: {
|
|
164
205
|
limit: '最多支持 20 个条件',
|
|
@@ -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: '实际大小',
|
|
@@ -25,6 +25,7 @@ import { Alert, AlertDescription } from '#components/ui/alert';
|
|
|
25
25
|
import { SettingsError, SettingsLoading } from './shared.js';
|
|
26
26
|
import { useSettingsData, useSettingsLeave } from './hooks.js';
|
|
27
27
|
import { permissionFilterLabels } from './permission-filters.js';
|
|
28
|
+
import { permissionFilterFieldLabel } from './permission-workspace.js';
|
|
28
29
|
import {
|
|
29
30
|
actionKey,
|
|
30
31
|
resourceKey,
|
|
@@ -79,7 +80,10 @@ function DefaultAccessFormBody(): ReactElement {
|
|
|
79
80
|
draft !== undefined && JSON.stringify(draft) !== JSON.stringify(initial);
|
|
80
81
|
const leave = useSettingsLeave(dirty, busy);
|
|
81
82
|
const close = () => {
|
|
82
|
-
void navigate(
|
|
83
|
+
void navigate(
|
|
84
|
+
{ pathname: '/default-access', search: location.search },
|
|
85
|
+
{ replace: true },
|
|
86
|
+
);
|
|
83
87
|
};
|
|
84
88
|
const text = (value: LocalizedText): string =>
|
|
85
89
|
typeof value === 'string'
|
|
@@ -174,6 +178,7 @@ function DefaultAccessFormBody(): ReactElement {
|
|
|
174
178
|
id: scope.id,
|
|
175
179
|
error: scopeErrors[scope.id],
|
|
176
180
|
action: text(scope.actionTitle),
|
|
181
|
+
actionId: scope.action,
|
|
177
182
|
scope: text(scope.title),
|
|
178
183
|
value: selectedValue,
|
|
179
184
|
options: choices,
|
|
@@ -185,7 +190,13 @@ function DefaultAccessFormBody(): ReactElement {
|
|
|
185
190
|
value={params}
|
|
186
191
|
fields={scope.fields.map((field) => ({
|
|
187
192
|
value: field,
|
|
188
|
-
label:
|
|
193
|
+
label:
|
|
194
|
+
fieldLabels[field] ??
|
|
195
|
+
permissionFilterFieldLabel(
|
|
196
|
+
scope.collection,
|
|
197
|
+
field,
|
|
198
|
+
(key, fallback) => t(key, { defaultValue: fallback ?? key }),
|
|
199
|
+
),
|
|
189
200
|
}))}
|
|
190
201
|
labels={labels}
|
|
191
202
|
disabled={readOnly}
|
|
@@ -210,6 +221,7 @@ function DefaultAccessFormBody(): ReactElement {
|
|
|
210
221
|
controls.push({
|
|
211
222
|
id: actionKey(action),
|
|
212
223
|
action: action.action,
|
|
224
|
+
actionId: action.action,
|
|
213
225
|
scope: action.scopeKey ?? value.resource.id,
|
|
214
226
|
value: JSON.stringify(action.selection),
|
|
215
227
|
options: [
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
LocalizedText,
|
|
4
4
|
} from '@nocobase/app-plugin-authorization/client';
|
|
5
5
|
import type { CatalogResource } from './permissions-model.js';
|
|
6
|
+
import { permissionScopeTitle } from './permission-workspace.js';
|
|
6
7
|
|
|
7
8
|
export type ScopeSelection =
|
|
8
9
|
| { readonly type: 'all' }
|
|
@@ -51,7 +52,10 @@ export function ruleResources(
|
|
|
51
52
|
...catalog.collections.map((collection) => ({
|
|
52
53
|
type: 'database.collection',
|
|
53
54
|
id: collection.name,
|
|
54
|
-
title:
|
|
55
|
+
title: permissionScopeTitle(
|
|
56
|
+
{ type: 'database.collection', id: collection.name },
|
|
57
|
+
{ collection: collection.name, title: collection.name },
|
|
58
|
+
),
|
|
55
59
|
actions: ['read', 'update', 'delete'].map((name) => ({
|
|
56
60
|
name,
|
|
57
61
|
title: {
|
|
@@ -98,7 +102,7 @@ export function ruleScopes(
|
|
|
98
102
|
action: action.name,
|
|
99
103
|
scopeKey: scope.key,
|
|
100
104
|
actionTitle: action.title,
|
|
101
|
-
title: scope.
|
|
105
|
+
title: permissionScopeTitle(resource, scope, undefined, action.name),
|
|
102
106
|
collection: scope.collection,
|
|
103
107
|
fields: scope.fields,
|
|
104
108
|
recordAccess: scope.recordAccess,
|
|
@@ -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
|
) : (
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
|
|
2
2
|
import type { LocalizedText } from '@nocobase/app-plugin-authorization/client';
|
|
3
|
-
import type
|
|
3
|
+
import { permissionRowId, type CatalogResource } from './permissions-model.js';
|
|
4
|
+
import type {
|
|
5
|
+
PermissionEditorNavigationNode,
|
|
6
|
+
PermissionEditorSection,
|
|
7
|
+
PermissionEditorGroup,
|
|
8
|
+
} from '#components/permission-editor';
|
|
9
|
+
import {
|
|
10
|
+
buildRouteNavigation,
|
|
11
|
+
type RouteNavigationItem,
|
|
12
|
+
} from '../../routing/route-navigation.js';
|
|
4
13
|
|
|
5
14
|
/** Application route names are stable within their owner; labels never change stored grants. */
|
|
6
15
|
export type PagePermissionTitles = Readonly<Record<string, string>>;
|
|
@@ -49,3 +58,159 @@ export function pagePermissionResources(
|
|
|
49
58
|
visit(routes);
|
|
50
59
|
return [...resources.values()];
|
|
51
60
|
}
|
|
61
|
+
|
|
62
|
+
/** Resource ownership follows the application's real settings branch, not translated labels or URL guesses. */
|
|
63
|
+
function configurationResources(
|
|
64
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
65
|
+
): ReadonlySet<string> {
|
|
66
|
+
const resources = new Set<string>();
|
|
67
|
+
const visit = (
|
|
68
|
+
items: readonly AppClientRegisteredRoute[],
|
|
69
|
+
inSettings = false,
|
|
70
|
+
): void => {
|
|
71
|
+
for (const route of items) {
|
|
72
|
+
const settings =
|
|
73
|
+
inSettings ||
|
|
74
|
+
(route.source === 'application' &&
|
|
75
|
+
['settings', 'systemManagement'].includes(route.name));
|
|
76
|
+
if (
|
|
77
|
+
settings &&
|
|
78
|
+
route.componentLoader &&
|
|
79
|
+
typeof route.authz === 'object'
|
|
80
|
+
) {
|
|
81
|
+
resources.add(
|
|
82
|
+
JSON.stringify([route.authz.resource.type, route.authz.resource.id]),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
visit(route.children ?? [], settings);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
visit(routes);
|
|
89
|
+
return resources;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The business page tree excludes system settings, whose original grants are configured in their own category. */
|
|
93
|
+
export function pagePermissionNavigation(
|
|
94
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
95
|
+
titles: PagePermissionTitles,
|
|
96
|
+
unnamed: (path: string) => string,
|
|
97
|
+
text: (value: LocalizedText) => string,
|
|
98
|
+
): readonly PermissionEditorNavigationNode[] {
|
|
99
|
+
const configuration = configurationResources(routes);
|
|
100
|
+
const pages = pagePermissionResources(routes, titles, unnamed).filter(
|
|
101
|
+
({ type, id }) => !configuration.has(JSON.stringify([type, id])),
|
|
102
|
+
);
|
|
103
|
+
const resources = new Map(pages.map((page) => [page.id, page]));
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
const visit = (
|
|
106
|
+
items: readonly RouteNavigationItem[],
|
|
107
|
+
): PermissionEditorNavigationNode[] =>
|
|
108
|
+
items.flatMap(({ route, children }) => {
|
|
109
|
+
const resource =
|
|
110
|
+
route.componentLoader &&
|
|
111
|
+
typeof route.authz === 'object' &&
|
|
112
|
+
route.authz.resource.type === 'page'
|
|
113
|
+
? resources.get(route.authz.resource.id)
|
|
114
|
+
: undefined;
|
|
115
|
+
const rowId =
|
|
116
|
+
resource && !seen.has(resource.id)
|
|
117
|
+
? permissionRowId(resource, 'access')
|
|
118
|
+
: undefined;
|
|
119
|
+
if (rowId && resource) seen.add(resource.id);
|
|
120
|
+
const descendants = visit(children);
|
|
121
|
+
if (!rowId && !descendants.length) return [];
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
id: `menu:${route.packageName}:${route.id}:${route.path}`,
|
|
125
|
+
label: text(
|
|
126
|
+
rowId && resource
|
|
127
|
+
? resource.title
|
|
128
|
+
: route.navigation?.title
|
|
129
|
+
? { key: route.navigation.title, ns: route.packageName }
|
|
130
|
+
: unnamed(route.path),
|
|
131
|
+
),
|
|
132
|
+
rowId,
|
|
133
|
+
children: descendants,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
});
|
|
137
|
+
// Author the catalog from the complete menu, not the signed-in editor's filtered navigation.
|
|
138
|
+
const navigation = visit(buildRouteNavigation(routes, new Set()));
|
|
139
|
+
const outside = pages
|
|
140
|
+
.filter((page) => !seen.has(page.id))
|
|
141
|
+
.map((page) => ({
|
|
142
|
+
id: `page:${page.id}`,
|
|
143
|
+
label: text(page.title),
|
|
144
|
+
rowId: permissionRowId(page, 'access'),
|
|
145
|
+
}));
|
|
146
|
+
return [
|
|
147
|
+
...navigation,
|
|
148
|
+
...(outside.length
|
|
149
|
+
? [
|
|
150
|
+
{
|
|
151
|
+
id: 'pages:outside-navigation',
|
|
152
|
+
label: text({ key: 'settings.pagesOutsideNavigation' }),
|
|
153
|
+
children: outside,
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
: []),
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Reclassify existing controls only: system settings never create a second page-access permission. */
|
|
161
|
+
export function permissionNavigationSections(
|
|
162
|
+
sections: readonly PermissionEditorSection[],
|
|
163
|
+
routes: readonly AppClientRegisteredRoute[],
|
|
164
|
+
titles: PagePermissionTitles,
|
|
165
|
+
unnamed: (path: string) => string,
|
|
166
|
+
text: (value: LocalizedText) => string,
|
|
167
|
+
): readonly PermissionEditorSection[] {
|
|
168
|
+
const configuration = configurationResources(routes);
|
|
169
|
+
const moved: PermissionEditorGroup[] = [];
|
|
170
|
+
const result = sections
|
|
171
|
+
.map((section) => {
|
|
172
|
+
if (section.id === 'configuration') return section;
|
|
173
|
+
const groups = section.groups.flatMap((group) => {
|
|
174
|
+
const belongs = (row: (typeof group.rows)[number]): boolean => {
|
|
175
|
+
const [type, id] = JSON.parse(row.id) as string[];
|
|
176
|
+
return configuration.has(JSON.stringify([type, id]));
|
|
177
|
+
};
|
|
178
|
+
const settingsRows = group.rows.filter(belongs);
|
|
179
|
+
if (settingsRows.length) moved.push({ ...group, rows: settingsRows });
|
|
180
|
+
const rows = group.rows.filter((row) => !belongs(row));
|
|
181
|
+
return rows.length ? [{ ...group, rows }] : [];
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
...section,
|
|
185
|
+
groups,
|
|
186
|
+
...(section.id === 'pages'
|
|
187
|
+
? {
|
|
188
|
+
navigation: pagePermissionNavigation(
|
|
189
|
+
routes,
|
|
190
|
+
titles,
|
|
191
|
+
unnamed,
|
|
192
|
+
text,
|
|
193
|
+
),
|
|
194
|
+
}
|
|
195
|
+
: {}),
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((section) => section.groups.length);
|
|
199
|
+
if (!moved.length) return result;
|
|
200
|
+
const target = result.find((section) => section.id === 'configuration');
|
|
201
|
+
return target
|
|
202
|
+
? result.map((section) =>
|
|
203
|
+
section === target
|
|
204
|
+
? { ...section, groups: [...section.groups, ...moved] }
|
|
205
|
+
: section,
|
|
206
|
+
)
|
|
207
|
+
: [
|
|
208
|
+
...result,
|
|
209
|
+
{
|
|
210
|
+
id: 'configuration',
|
|
211
|
+
title: text({ key: 'settings.configurationPermissions' }),
|
|
212
|
+
description: text({ key: 'settings.configurationPermissionsHint' }),
|
|
213
|
+
groups: moved,
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
AuthorizationOptionsResponse,
|
|
9
9
|
} from '@nocobase/app-plugin-authorization/client';
|
|
10
10
|
import { getScopeSelection } from './permissions-model.js';
|
|
11
|
+
import { permissionFilterFieldLabel } from './permission-workspace.js';
|
|
11
12
|
|
|
12
13
|
export function configurePermissionScopes(
|
|
13
14
|
sections: readonly PermissionEditorSection[],
|
|
@@ -39,7 +40,9 @@ export function configurePermissionScopes(
|
|
|
39
40
|
const definition = resources
|
|
40
41
|
.find(
|
|
41
42
|
(resource) =>
|
|
42
|
-
resource.type === type &&
|
|
43
|
+
resource.type === type &&
|
|
44
|
+
resource.id === resourceId &&
|
|
45
|
+
resource.actions.some((item) => item.name === action),
|
|
43
46
|
)
|
|
44
47
|
?.dataScopes?.[action ?? '']?.find((item) => item.key === key);
|
|
45
48
|
const value = getScopeSelection(grants, scope.id);
|
|
@@ -62,7 +65,11 @@ export function configurePermissionScopes(
|
|
|
62
65
|
value={filter}
|
|
63
66
|
fields={(definition?.fields ?? []).map((field) => ({
|
|
64
67
|
value: field,
|
|
65
|
-
label:
|
|
68
|
+
label: permissionFilterFieldLabel(
|
|
69
|
+
definition?.collection ?? '',
|
|
70
|
+
field,
|
|
71
|
+
text,
|
|
72
|
+
),
|
|
66
73
|
}))}
|
|
67
74
|
labels={labels}
|
|
68
75
|
disabled={disabled}
|