@nocobase/app-template-default 1.0.0-beta.68 → 1.0.0-beta.70

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.
Files changed (76) hide show
  1. package/.agents/rules/components.md +5 -1
  2. package/.agents/rules/shell.md +3 -1
  3. package/AGENTS.md +7 -3
  4. package/Dockerfile.dockerignore +0 -1
  5. package/README.MD +3 -1
  6. package/client/app.ts +15 -0
  7. package/client/components/data-table/column-header.tsx +91 -0
  8. package/client/components/data-table/data-table.tsx +216 -0
  9. package/client/components/data-table/index.ts +13 -0
  10. package/client/components/data-table/pagination.tsx +165 -0
  11. package/client/components/data-table/view-options.tsx +77 -0
  12. package/client/components/flow-diagram-css.d.ts +2 -0
  13. package/client/components/flow-diagram.tsx +674 -0
  14. package/client/components/flow-layout.ts +250 -0
  15. package/client/components/lifecycle-diagram-model.ts +217 -0
  16. package/client/components/lifecycle-diagram.tsx +310 -0
  17. package/client/components/permission-editor.tsx +589 -0
  18. package/client/components/permission-filter.tsx +455 -0
  19. package/client/components/route-overlay.tsx +26 -13
  20. package/client/components/ui/checkbox.tsx +25 -0
  21. package/client/components/ui/sidebar.tsx +1 -4
  22. package/client/components/ui/switch.tsx +30 -0
  23. package/client/components/ui/table.tsx +113 -0
  24. package/client/extensions/nocobase-workflow-management/README.md +70 -0
  25. package/client/extensions/nocobase-workflow-management/definition-list.tsx +148 -0
  26. package/client/extensions/nocobase-workflow-management/execution-detail.tsx +230 -0
  27. package/client/extensions/nocobase-workflow-management/execution-list.tsx +175 -0
  28. package/client/extensions/nocobase-workflow-management/layout.tsx +95 -0
  29. package/client/layouts/components/app-sidebar.tsx +1 -1
  30. package/client/layouts/use-sidebar-preference.ts +62 -42
  31. package/client/locales/system/en-US.ts +29 -0
  32. package/client/locales/system/settings-en-US.ts +142 -0
  33. package/client/locales/system/settings-zh-CN.ts +136 -0
  34. package/client/locales/system/zh-CN.ts +29 -0
  35. package/client/pages/auth/auth-page.css +15 -0
  36. package/client/pages/auth/login.tsx +1 -1
  37. package/client/pages/auth/shared.tsx +3 -1
  38. package/client/pages/settings/hooks.tsx +137 -0
  39. package/client/pages/settings/page-permission-titles.ts +7 -0
  40. package/client/pages/settings/page-permissions.ts +42 -0
  41. package/client/pages/settings/permission-filters.tsx +119 -0
  42. package/client/pages/settings/permissions-model.ts +260 -0
  43. package/client/pages/settings/role-form.tsx +405 -0
  44. package/client/pages/settings/roles.tsx +163 -0
  45. package/client/pages/settings/routes.ts +79 -0
  46. package/client/pages/settings/shared.tsx +119 -0
  47. package/client/pages/settings/user-detail.tsx +268 -0
  48. package/client/pages/settings/user-form.tsx +427 -0
  49. package/client/pages/settings/users.tsx +451 -0
  50. package/client/routing/app-router.tsx +4 -1
  51. package/client/routing/session-preparation.tsx +65 -0
  52. package/client/styles.css +4 -7
  53. package/package.json +27 -24
  54. package/tests/components/app-layout-routes.test.tsx +14 -7
  55. package/tests/components/data-table.test.tsx +127 -0
  56. package/tests/components/layout-i18n.test.tsx +8 -1
  57. package/tests/components/navigation-menu.test.tsx +24 -14
  58. package/tests/components/permission-editor.test.tsx +477 -0
  59. package/tests/components/permission-filter-binding.test.tsx +150 -0
  60. package/tests/components/permission-filter.test.tsx +202 -0
  61. package/tests/components/settings.test.tsx +294 -0
  62. package/tests/components/sidebar-permissions.test.tsx +12 -4
  63. package/tests/components/sidebar-preference.test.tsx +209 -14
  64. package/tests/components/user-permissions.test.tsx +142 -0
  65. package/tests/fixtures/theme-tokens.tsx +9 -1
  66. package/tests/logic/asset-url-build.test.ts +0 -1
  67. package/tests/logic/client-routes.test.ts +40 -0
  68. package/tests/logic/client-shell.test.tsx +10 -2
  69. package/tests/logic/config-example.test.ts +30 -0
  70. package/tests/logic/dev-client-watch.test.ts +8 -1
  71. package/tests/logic/page-permission-titles.test.ts +223 -0
  72. package/tests/logic/route-overlay.test.tsx +42 -0
  73. package/tests/logic/session-preparation.test.tsx +223 -0
  74. package/tests/logic/settings-permissions.test.ts +168 -0
  75. package/vite.config.ts +0 -22
  76. package/client/agent-annotations-host.ts +0 -25
@@ -0,0 +1,136 @@
1
+ const settings = {
2
+ unnamedPage: '未命名页面({{path}})',
3
+ revokeRoles: '撤销角色',
4
+ keyConflict: '此角色标识已被使用。',
5
+ userPermissions: '用户管理',
6
+ readUsers: '查看用户',
7
+ createUsers: '创建用户',
8
+ updateUsers: '编辑用户',
9
+ inviteUsers: '邀请用户',
10
+ deleteUsers: '删除用户',
11
+ resetUserPassword: '重置密码',
12
+ assignRole: '分配角色',
13
+
14
+ assignedRoles: '已分配角色',
15
+ actionSaved: '已更新“{{name}}”的访问状态',
16
+
17
+ identityConflict: '此邮箱或用户名已被使用。',
18
+ signInAgain: '重新登录',
19
+ passwordPolicy: '密码不符合当前密码策略。',
20
+
21
+ reload: '重新加载角色',
22
+ title: '系统设置',
23
+ users: '用户管理',
24
+ roles: '角色与权限',
25
+ usersDescription: '管理用户账户、角色及登录会话。',
26
+ rolesDescription: '设置每个角色可访问的页面与可执行的操作。',
27
+ userSearch: '搜索姓名、用户名或邮箱',
28
+ roleSearch: '搜索角色名称',
29
+ newUser: '新建用户',
30
+ editUser: '编辑用户',
31
+ newRole: '新建角色',
32
+ editRole: '编辑角色',
33
+ name: '姓名',
34
+ username: '用户名',
35
+ email: '邮箱',
36
+ password: '初始密码',
37
+ roleName: '角色名称',
38
+ key: '标识',
39
+ status: '状态',
40
+ actions: '操作',
41
+ actionsFor: '{{name}} 的操作',
42
+ all: '全部状态',
43
+ enabled: '启用',
44
+ disabled: '停用',
45
+ clear: '清除筛选',
46
+ noResults: '没有匹配的结果',
47
+ clearHint: '请更换搜索条件或清除筛选。',
48
+ noUsers: '暂无用户',
49
+ noRoles: '暂无角色',
50
+ create: '创建',
51
+ edit: '编辑',
52
+ save: '保存',
53
+ cancel: '取消',
54
+ saving: '正在保存…',
55
+ confirm: '确认',
56
+ enable: '启用',
57
+ disable: '禁用',
58
+ revokeSessions: '撤销会话',
59
+ disableTitle: '禁用用户“{{name}}”?',
60
+ disableHint: '该用户将无法登录,现有会话将失效。',
61
+ enableTitle: '启用用户“{{name}}”?',
62
+ enableHint: '该用户可以重新登录。',
63
+ revokeTitle: '撤销用户“{{name}}”的会话?',
64
+ revokeHint: '该用户需要重新登录。',
65
+ revokeRoleTitle: '撤销“{{name}}”的角色?',
66
+ revokeRoleHint: '将移除所列角色授予的访问权限。',
67
+ created: '已创建用户“{{name}}”',
68
+ saved: '已保存“{{name}}”',
69
+ rolesSaved: '已更新“{{name}}”的角色',
70
+ required: '请填写此字段',
71
+ invalidEmail: '请输入有效邮箱',
72
+ protected: '受保护',
73
+ custom: '自定义',
74
+ protectedHint: '此角色受系统保护,仅允许明确授权的修改。',
75
+ unsaved: '有未保存的更改',
76
+ discardTitle: '放弃未保存的更改?',
77
+ discardHint: '未保存的更改将丢失。',
78
+ failed: '请求失败,请重试。',
79
+ retry: '重试',
80
+ loading: '加载中',
81
+ denied: '无权查看,请联系管理员。',
82
+ missing: '记录不存在或已被删除。',
83
+ conflict: '角色已被其他人修改,请重新加载后合并。',
84
+ sessionEnded: '会话已结束,请重新登录。',
85
+ previous: '上一页',
86
+ next: '下一页',
87
+ total_one: '共 {{count}} 位用户',
88
+ total_other: '共 {{count}} 位用户',
89
+ access: '访问',
90
+ permissionSearch: '搜索分类、分组或权限',
91
+ enabledOnly: '仅看已开启',
92
+ noPermissions: '暂无可配置权限',
93
+ permissionCategories: '权限分类',
94
+ selectAllPermissions: '全选',
95
+ selectVisiblePermissions: '选择“{{name}}”中显示的权限',
96
+ permissionSummary: '已开启 {{enabled}} / {{total}}',
97
+ noRecordScope: '无记录(未授权)',
98
+ permissionFilter: {
99
+ limit: '最多支持 20 个条件',
100
+ configure: '筛选条件',
101
+ title: '配置记录筛选',
102
+ description: '应用条件后还需保存角色,权限才会生效。',
103
+ all: '满足全部条件',
104
+ any: '满足任一条件',
105
+ field: '字段',
106
+ operator: '比较方式',
107
+ valueType: '值类型',
108
+ value: '值',
109
+ text: '文本',
110
+ number: '数字',
111
+ boolean: '是或否',
112
+ true: '是',
113
+ false: '否',
114
+ add: '添加条件',
115
+ remove: '移除',
116
+ cancel: '取消',
117
+ apply: '应用',
118
+ invalid: '请选择有效字段并填写完整的值,至少保留一个条件。',
119
+ advanced: '现有筛选包含高级条件,将保留原样;重新配置后应用才会替换。',
120
+ reconfigure: '重新配置',
121
+ operators: {
122
+ $eq: '等于',
123
+ $ne: '不等于',
124
+ $includes: '包含',
125
+ $gt: '大于',
126
+ $gte: '大于或等于',
127
+ $lt: '小于',
128
+ $lte: '小于或等于',
129
+ $empty: '为空',
130
+ $notEmpty: '不为空',
131
+ },
132
+ },
133
+ recordScope: '数据范围',
134
+ levelFor: '{{name}} 的范围',
135
+ };
136
+ export default settings;
@@ -1,9 +1,38 @@
1
+ import settings from './settings-zh-CN.js';
1
2
  import type { SystemResource } from './en-US.js';
2
3
  import deviceApprovalZhCN from '#extensions/nocobase-device-approval/locales/zh-CN';
3
4
  import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
4
5
 
5
6
  /** The Chinese copy of what the template ships; see `./en-US.ts`. */
6
7
  const systemZhCN: SystemResource = {
8
+ flowDiagram: {
9
+ configure: '配置',
10
+ overview: '流程缩略导航',
11
+ actualSize: '实际大小',
12
+ states: '状态',
13
+ transitions: '转移',
14
+ configurable: '可配置参数',
15
+ current: '当前',
16
+ visited: '已经过',
17
+ initial: '初始状态',
18
+ final: '终止状态',
19
+ state: '状态',
20
+ zoomOut: '缩小',
21
+ zoomIn: '放大',
22
+ fit: '适应画布',
23
+
24
+ empty: '暂无可展示的状态。',
25
+ loading: '正在布局流程图…',
26
+ failed: '无法布局此流程图。',
27
+ nodeHelp: '按 Enter 或空格键选择状态。',
28
+ edgeHelp: '按 Enter 或空格键选择转移。',
29
+ },
30
+ settings,
31
+ 'auth.preparingSession': '正在准备登录会话…',
32
+ 'auth.connectionFailed': '连接失败',
33
+ 'auth.connectionFailedDescription':
34
+ '实时连接暂时无法准备就绪。请重试后进入应用,或退出登录。',
35
+
7
36
  'auth.passwordResetSuccess': '密码已重置,请用新密码登录。',
8
37
  'auth.requestNewResetLink': '重新申请重置链接',
9
38
 
@@ -35,3 +35,18 @@
35
35
  .dark .auth-page [data-slot='input-group'] input {
36
36
  background-color: transparent;
37
37
  }
38
+
39
+ /* Keep login controls comfortable while tightening the form's vertical rhythm. */
40
+ .auth-login-page main header {
41
+ margin-bottom: calc(var(--spacing) * 6);
42
+ }
43
+
44
+ .auth-login-page form > [data-slot='field-group'] {
45
+ gap: calc(var(--spacing) * 4);
46
+ }
47
+
48
+ .auth-login-page form [data-slot='input'],
49
+ .auth-login-page form [data-slot='input-group'],
50
+ .auth-login-page form button[type='submit'] {
51
+ height: calc(var(--spacing) * 10);
52
+ }
@@ -21,6 +21,7 @@ export default function LoginPage(): ReactElement {
21
21
 
22
22
  return (
23
23
  <AuthPage
24
+ className='auth-login-page'
24
25
  description={t('auth.loginDescription')}
25
26
  title={t('auth.welcome')}
26
27
  >
@@ -36,7 +37,6 @@ export default function LoginPage(): ReactElement {
36
37
  {
37
38
  content: (
38
39
  <PasswordLoginForm
39
- className='[&_[data-slot=input]]:h-12 [&_[data-slot=input-group]]:h-12 [&_button[type=submit]]:h-12'
40
40
  error={login.error?.message}
41
41
  footer={
42
42
  signUpAvailable ? (
@@ -8,6 +8,7 @@ import { AuthSplitLayout } from '#extensions/nocobase-auth-split-layout/auth-spl
8
8
  import './auth-page.css';
9
9
 
10
10
  export interface AuthPageProps {
11
+ readonly className?: string;
11
12
  readonly title: ReactNode;
12
13
  readonly description: ReactNode;
13
14
  readonly children: ReactNode;
@@ -20,6 +21,7 @@ export interface AuthPageProps {
20
21
  */
21
22
  export function AuthPage({
22
23
  children,
24
+ className,
23
25
  description,
24
26
  title,
25
27
  }: AuthPageProps): ReactElement {
@@ -28,7 +30,7 @@ export function AuthPage({
28
30
  <AuthSplitLayout
29
31
  aside={<BrandPanel />}
30
32
  asideLabel={t('auth.about')}
31
- className='auth-page'
33
+ className={['auth-page', className].filter(Boolean).join(' ')}
32
34
  description={description}
33
35
  logo={
34
36
  <>
@@ -0,0 +1,137 @@
1
+ import {
2
+ ApiClientError,
3
+ useApiClient,
4
+ useNavigationGuard,
5
+ } from '@nocobase/app-client';
6
+ import { useCan } from '@nocobase/app-plugin-authorization/client';
7
+ import { useTranslation } from '@nocobase/i18n/client';
8
+ import { useCallback, useEffect, useRef, useState } from 'react';
9
+ import { useLocation } from 'react-router';
10
+ import { ConfirmAction } from './shared.js';
11
+
12
+ export function useSettingsData<T>(path: string, keepPreviousData = false) {
13
+ const api = useApiClient();
14
+ const [revision, setRevision] = useState(0);
15
+ const [result, setResult] = useState<{
16
+ path: string;
17
+ revision: number;
18
+ data?: T;
19
+ error?: unknown;
20
+ loading: boolean;
21
+ }>({ path, revision: -1, loading: true });
22
+ useEffect(() => {
23
+ let active = true;
24
+ void api.request<T>({ path }).then(
25
+ (data) => {
26
+ if (active) setResult({ path, revision, data, loading: false });
27
+ },
28
+ (error: unknown) => {
29
+ if (active)
30
+ setResult((old) => ({
31
+ ...(old.path === path &&
32
+ !(
33
+ error instanceof ApiClientError &&
34
+ [401, 403, 404].includes(error.status)
35
+ )
36
+ ? old
37
+ : {}),
38
+ path,
39
+ revision,
40
+ error,
41
+ loading: false,
42
+ }));
43
+ },
44
+ );
45
+ return () => {
46
+ active = false;
47
+ };
48
+ }, [api, path, revision]);
49
+ return {
50
+ ...(result.path === path || keepPreviousData
51
+ ? result
52
+ : { path, loading: true, data: undefined, error: undefined }),
53
+ loading: result.path !== path || result.revision !== revision,
54
+ error:
55
+ result.path === path && result.revision === revision
56
+ ? result.error
57
+ : undefined,
58
+ reload: useCallback(() => setRevision((value) => value + 1), []),
59
+ replaceData: useCallback(
60
+ (data: T) => setResult({ path, revision, data, loading: false }),
61
+ [path, revision],
62
+ ),
63
+ };
64
+ }
65
+
66
+ export function useUserPermission(action: string, id = '*') {
67
+ return useCan({ resource: { type: 'user', id }, action }).can;
68
+ }
69
+ export function useRolePermission(action: string) {
70
+ return useCan({
71
+ resource: { type: 'settings', id: 'authorization.permission-sets' },
72
+ action,
73
+ }).can;
74
+ }
75
+ /** The same decision covers overlay close, sidebar links, browser history and refresh. */
76
+ export function useSettingsLeave(
77
+ dirty: boolean,
78
+ busy: boolean,
79
+ onDiscard?: () => void,
80
+ ) {
81
+ const { t } = useTranslation();
82
+ const location = useLocation();
83
+ const allowedRef = useRef(false);
84
+ useEffect(() => {
85
+ allowedRef.current = false;
86
+ }, [location.key]);
87
+ const pendingRef = useRef<
88
+ { promise: Promise<boolean>; resolve: (value: boolean) => void } | undefined
89
+ >(undefined);
90
+ const [open, setOpen] = useState(false);
91
+ const guard = () => {
92
+ if (allowedRef.current) return true;
93
+ if (busy) return false;
94
+ if (!dirty) return true;
95
+ if (pendingRef.current) return pendingRef.current.promise;
96
+ let resolve!: (value: boolean) => void;
97
+ const promise = new Promise<boolean>((done) => {
98
+ resolve = done;
99
+ });
100
+ pendingRef.current = { promise, resolve };
101
+ setOpen(true);
102
+ return promise;
103
+ };
104
+ useNavigationGuard(guard);
105
+ useEffect(() => {
106
+ if (!dirty) return;
107
+ const handler = (event: BeforeUnloadEvent) => {
108
+ event.preventDefault();
109
+ event.returnValue = '';
110
+ };
111
+ window.addEventListener('beforeunload', handler);
112
+ return () => window.removeEventListener('beforeunload', handler);
113
+ }, [dirty]);
114
+ const decide = (value: boolean) => {
115
+ if (value) onDiscard?.();
116
+ allowedRef.current = value;
117
+ pendingRef.current?.resolve(value);
118
+ pendingRef.current = undefined;
119
+ setOpen(false);
120
+ };
121
+ return {
122
+ guard,
123
+ allow: () => {
124
+ allowedRef.current = true;
125
+ },
126
+ dialog: (
127
+ <ConfirmAction
128
+ open={open}
129
+ title={t('settings.discardTitle')}
130
+ description={t('settings.discardHint')}
131
+ busy={false}
132
+ onCancel={() => decide(false)}
133
+ onConfirm={() => decide(true)}
134
+ />
135
+ ),
136
+ };
137
+ }
@@ -0,0 +1,7 @@
1
+ import type { PagePermissionTitles } from './page-permissions.js';
2
+
3
+ /** Permission labels for application pages without navigation/breadcrumb titles (including overlays).
4
+ * Keys are existing route names; values are application translation keys.
5
+ * Do not add navigation/breadcrumbs or change authorization ids just to name a permission.
6
+ */
7
+ export const pagePermissionTitles: PagePermissionTitles = {};
@@ -0,0 +1,42 @@
1
+ import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
2
+ import type { LocalizedText } from '@nocobase/app-plugin-authorization/client';
3
+ import type { CatalogResource } from './permissions-model.js';
4
+
5
+ /** Application route names are stable within their owner; labels never change stored grants. */
6
+ export type PagePermissionTitles = Readonly<Record<string, string>>;
7
+
8
+ export function pagePermissionTitle(
9
+ route: AppClientRegisteredRoute,
10
+ titles: PagePermissionTitles,
11
+ ): LocalizedText | undefined {
12
+ const key =
13
+ (route.source === 'application' && Object.hasOwn(titles, route.name)
14
+ ? titles[route.name]
15
+ : undefined) ??
16
+ route.navigation?.title ??
17
+ route.breadcrumb?.title;
18
+ return key?.trim() ? { key, ns: route.packageName } : undefined;
19
+ }
20
+
21
+ /** Preserve every existing resource id/action; never use a technical route id as its label. */
22
+ export function pagePermissionResources(
23
+ routes: readonly AppClientRegisteredRoute[],
24
+ titles: PagePermissionTitles,
25
+ unnamed: (path: string) => string,
26
+ ): CatalogResource[] {
27
+ return routes.flatMap((route) => [
28
+ ...(route.componentLoader &&
29
+ typeof route.authz === 'object' &&
30
+ route.authz.resource.type === 'page'
31
+ ? [
32
+ {
33
+ type: 'page',
34
+ id: route.authz.resource.id,
35
+ title: pagePermissionTitle(route, titles) ?? unnamed(route.path),
36
+ actions: [{ name: 'access', title: { key: 'settings.access' } }],
37
+ },
38
+ ]
39
+ : []),
40
+ ...pagePermissionResources(route.children ?? [], titles, unnamed),
41
+ ]);
42
+ }
@@ -0,0 +1,119 @@
1
+ import type { PermissionEditorSection } from '#components/permission-editor';
2
+ import {
3
+ PermissionFilter,
4
+ type PermissionFilterLabels,
5
+ } from '#components/permission-filter';
6
+ import type {
7
+ PermissionGrant,
8
+ AuthorizationOptionsResponse,
9
+ } from '@nocobase/app-plugin-authorization/client';
10
+ import { getScopeSelection } from './permissions-model.js';
11
+
12
+ export function configurePermissionScopes(
13
+ sections: readonly PermissionEditorSection[],
14
+ catalog: AuthorizationOptionsResponse,
15
+ grants: readonly PermissionGrant[],
16
+ text: (key: string, fallback?: string) => string,
17
+ editing: string | undefined,
18
+ setEditing: (id: string | undefined) => void,
19
+ onApply: (id: string, value: unknown) => void,
20
+ disabled: boolean,
21
+ ): readonly PermissionEditorSection[] {
22
+ const labels: PermissionFilterLabels = {
23
+ limit: text('settings.permissionFilter.limit'),
24
+ configure: text('settings.permissionFilter.configure'),
25
+ title: text('settings.permissionFilter.title'),
26
+ description: text('settings.permissionFilter.description'),
27
+ all: text('settings.permissionFilter.all'),
28
+ any: text('settings.permissionFilter.any'),
29
+ field: text('settings.permissionFilter.field'),
30
+ operator: text('settings.permissionFilter.operator'),
31
+ valueType: text('settings.permissionFilter.valueType'),
32
+ value: text('settings.permissionFilter.value'),
33
+ text: text('settings.permissionFilter.text'),
34
+ number: text('settings.permissionFilter.number'),
35
+ boolean: text('settings.permissionFilter.boolean'),
36
+ true: text('settings.permissionFilter.true'),
37
+ false: text('settings.permissionFilter.false'),
38
+ add: text('settings.permissionFilter.add'),
39
+ remove: text('settings.permissionFilter.remove'),
40
+ cancel: text('settings.permissionFilter.cancel'),
41
+ apply: text('settings.permissionFilter.apply'),
42
+ invalid: text('settings.permissionFilter.invalid'),
43
+ advanced: text('settings.permissionFilter.advanced'),
44
+ reconfigure: text('settings.permissionFilter.reconfigure'),
45
+ operators: Object.fromEntries(
46
+ [
47
+ '$eq',
48
+ '$ne',
49
+ '$includes',
50
+ '$gt',
51
+ '$gte',
52
+ '$lt',
53
+ '$lte',
54
+ '$empty',
55
+ '$notEmpty',
56
+ ].map((key) => [key, text(`settings.permissionFilter.operators.${key}`)]),
57
+ ),
58
+ };
59
+ const resources = catalog.sections.flatMap((section) =>
60
+ section.subsections.flatMap((sub) => sub.resources),
61
+ );
62
+ return sections.map((section) => ({
63
+ ...section,
64
+ groups: section.groups.map((group) => ({
65
+ ...group,
66
+ rows: group.rows.map((row) => ({
67
+ ...row,
68
+ scopes:
69
+ row.scopes?.map((scope) => {
70
+ if (scope.level !== 'customFilter' && editing !== scope.id)
71
+ return scope;
72
+ const [type, resourceId, action, key] = JSON.parse(
73
+ scope.id,
74
+ ) as string[];
75
+ const definition = resources
76
+ .find(
77
+ (resource) =>
78
+ resource.type === type && resource.id === resourceId,
79
+ )
80
+ ?.dataScopes?.[action ?? '']?.find((item) => item.key === key);
81
+ const value = getScopeSelection(grants, scope.id);
82
+ const params: unknown =
83
+ value && typeof value === 'object'
84
+ ? Reflect.get(value, 'params')
85
+ : undefined;
86
+ const filter: unknown =
87
+ params && typeof params === 'object'
88
+ ? Reflect.get(params, 'filter')
89
+ : undefined;
90
+ return {
91
+ ...scope,
92
+ configuration: (
93
+ <PermissionFilter
94
+ open={editing === scope.id}
95
+ onOpenChange={(open) =>
96
+ setEditing(open ? scope.id : undefined)
97
+ }
98
+ value={filter}
99
+ fields={(definition?.fields ?? []).map((field) => ({
100
+ value: field,
101
+ label: text(`settings.permissionFields.${field}`, field),
102
+ }))}
103
+ labels={labels}
104
+ disabled={disabled}
105
+ onApply={(filter) =>
106
+ onApply(scope.id, {
107
+ type: 'recordAccess',
108
+ key: 'customFilter',
109
+ params: { filter },
110
+ })
111
+ }
112
+ />
113
+ ),
114
+ };
115
+ }) ?? [],
116
+ })),
117
+ })),
118
+ }));
119
+ }