@nocobase/app-template-default 1.0.0-beta.69 → 1.0.0-beta.71
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/components.md +4 -0
- package/AGENTS.md +3 -1
- package/client/components/flow-diagram-css.d.ts +2 -0
- package/client/components/flow-diagram.tsx +674 -0
- package/client/components/flow-layout.ts +250 -0
- package/client/components/lifecycle-diagram-model.ts +217 -0
- package/client/components/lifecycle-diagram.tsx +310 -0
- package/client/components/permission-editor.tsx +111 -73
- package/client/extensions/nocobase-workflow-management/README.md +70 -0
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +148 -0
- package/client/extensions/nocobase-workflow-management/execution-detail.tsx +230 -0
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +175 -0
- package/client/extensions/nocobase-workflow-management/layout.tsx +95 -0
- package/client/locales/system/en-US.ts +22 -0
- package/client/locales/system/settings-en-US.ts +2 -0
- package/client/locales/system/settings-zh-CN.ts +8 -6
- package/client/locales/system/zh-CN.ts +22 -0
- package/client/pages/settings/page-permission-titles.ts +7 -0
- package/client/pages/settings/page-permissions.ts +51 -0
- package/client/pages/settings/permissions-model.ts +60 -31
- package/client/pages/settings/role-form.tsx +8 -29
- package/package.json +11 -9
- package/tests/components/permission-editor.test.tsx +62 -0
- package/tests/logic/page-permission-titles.test.ts +303 -0
- package/tests/logic/settings-permissions.test.ts +77 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import { PageContainer } from '#components/page-container';
|
|
3
|
+
import { PageHeader } from '#components/page-header';
|
|
4
|
+
import { Tabs, TabsList, TabsTrigger } from '#components/ui/tabs';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '#components/ui/card';
|
|
6
|
+
|
|
7
|
+
export type WorkflowTab = 'definitions' | 'executions';
|
|
8
|
+
|
|
9
|
+
/** The caller owns routes, tab URLs and permissions; details replace the list chrome. */
|
|
10
|
+
export interface WorkflowManagementLayoutProps {
|
|
11
|
+
title: string;
|
|
12
|
+
description?: ReactNode;
|
|
13
|
+
labels: Record<WorkflowTab, string>;
|
|
14
|
+
tab: WorkflowTab;
|
|
15
|
+
onTabChange: (tab: WorkflowTab) => void;
|
|
16
|
+
detail?: boolean;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function WorkflowManagementLayout({
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
labels,
|
|
24
|
+
tab,
|
|
25
|
+
onTabChange,
|
|
26
|
+
detail = false,
|
|
27
|
+
children,
|
|
28
|
+
}: WorkflowManagementLayoutProps): ReactElement {
|
|
29
|
+
return (
|
|
30
|
+
<PageContainer>
|
|
31
|
+
{!detail && (
|
|
32
|
+
<>
|
|
33
|
+
<PageHeader title={title} description={description} />
|
|
34
|
+
<Tabs
|
|
35
|
+
value={tab}
|
|
36
|
+
onValueChange={(value) => onTabChange(value as WorkflowTab)}
|
|
37
|
+
>
|
|
38
|
+
<TabsList variant='line'>
|
|
39
|
+
<TabsTrigger value='definitions'>
|
|
40
|
+
{labels.definitions}
|
|
41
|
+
</TabsTrigger>
|
|
42
|
+
<TabsTrigger value='executions'>{labels.executions}</TabsTrigger>
|
|
43
|
+
</TabsList>
|
|
44
|
+
</Tabs>
|
|
45
|
+
</>
|
|
46
|
+
)}
|
|
47
|
+
{children}
|
|
48
|
+
</PageContainer>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkflowDefinitionDetailProps {
|
|
53
|
+
diagram: ReactNode;
|
|
54
|
+
parameters?: ReactNode;
|
|
55
|
+
diagramTitle: string;
|
|
56
|
+
parametersTitle: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function WorkflowDefinitionDetail({
|
|
60
|
+
diagram,
|
|
61
|
+
parameters,
|
|
62
|
+
diagramTitle,
|
|
63
|
+
parametersTitle,
|
|
64
|
+
}: WorkflowDefinitionDetailProps): ReactElement {
|
|
65
|
+
return (
|
|
66
|
+
<div className='flex flex-col gap-4'>
|
|
67
|
+
<div
|
|
68
|
+
className={
|
|
69
|
+
parameters == null
|
|
70
|
+
? 'min-w-0'
|
|
71
|
+
: 'grid min-w-0 grid-cols-1 items-start gap-6 lg:grid-cols-2'
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
<Card className='min-w-0' role='region' aria-label={diagramTitle}>
|
|
75
|
+
<CardHeader>
|
|
76
|
+
<CardTitle>
|
|
77
|
+
<h2>{diagramTitle}</h2>
|
|
78
|
+
</CardTitle>
|
|
79
|
+
</CardHeader>
|
|
80
|
+
<CardContent className='flex flex-1 flex-col'>{diagram}</CardContent>
|
|
81
|
+
</Card>
|
|
82
|
+
{parameters != null && (
|
|
83
|
+
<Card className='min-w-0' role='region' aria-label={parametersTitle}>
|
|
84
|
+
<CardHeader>
|
|
85
|
+
<CardTitle>
|
|
86
|
+
<h2>{parametersTitle}</h2>
|
|
87
|
+
</CardTitle>
|
|
88
|
+
</CardHeader>
|
|
89
|
+
<CardContent>{parameters}</CardContent>
|
|
90
|
+
</Card>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -8,6 +8,28 @@ 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
|
+
flowDiagram: {
|
|
12
|
+
configure: 'Configure',
|
|
13
|
+
overview: 'Diagram overview',
|
|
14
|
+
actualSize: 'Actual size',
|
|
15
|
+
states: 'States',
|
|
16
|
+
transitions: 'Transitions',
|
|
17
|
+
configurable: 'Parameters',
|
|
18
|
+
current: 'Current',
|
|
19
|
+
visited: 'Visited',
|
|
20
|
+
initial: 'Initial',
|
|
21
|
+
final: 'Final',
|
|
22
|
+
state: 'State',
|
|
23
|
+
zoomOut: 'Zoom out',
|
|
24
|
+
zoomIn: 'Zoom in',
|
|
25
|
+
fit: 'Fit to view',
|
|
26
|
+
|
|
27
|
+
empty: 'No states to display.',
|
|
28
|
+
loading: 'Laying out diagram…',
|
|
29
|
+
failed: 'Unable to lay out this diagram.',
|
|
30
|
+
nodeHelp: 'Press Enter or Space to select a state.',
|
|
31
|
+
edgeHelp: 'Press Enter or Space to select a transition.',
|
|
32
|
+
},
|
|
11
33
|
settings,
|
|
12
34
|
'auth.preparingSession': 'Preparing your session…',
|
|
13
35
|
'auth.connectionFailed': 'Unable to connect',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const settings = {
|
|
2
|
+
unnamedPage: 'Unnamed page ({{path}})',
|
|
2
3
|
revokeRoles: 'Revoke roles',
|
|
3
4
|
keyConflict: 'This role key is already in use.',
|
|
4
5
|
userPermissions: 'User management',
|
|
@@ -96,6 +97,7 @@ const settings = {
|
|
|
96
97
|
selectAllPermissions: 'All',
|
|
97
98
|
selectVisiblePermissions: 'Select visible permissions in {{name}}',
|
|
98
99
|
permissionSummary: '{{enabled}} of {{total}} enabled',
|
|
100
|
+
preservedRecordScope: 'Keep existing configuration',
|
|
99
101
|
noRecordScope: 'No records (not authorized)',
|
|
100
102
|
permissionFilter: {
|
|
101
103
|
limit: 'Up to 20 conditions',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const settings = {
|
|
2
|
+
unnamedPage: '未命名页面({{path}})',
|
|
2
3
|
revokeRoles: '撤销角色',
|
|
3
4
|
keyConflict: '此角色标识已被使用。',
|
|
4
5
|
userPermissions: '用户管理',
|
|
@@ -87,18 +88,19 @@ const settings = {
|
|
|
87
88
|
total_other: '共 {{count}} 位用户',
|
|
88
89
|
access: '访问',
|
|
89
90
|
permissionSearch: '搜索分类、分组或权限',
|
|
90
|
-
enabledOnly: '
|
|
91
|
+
enabledOnly: '仅显示已授权',
|
|
91
92
|
noPermissions: '暂无可配置权限',
|
|
92
93
|
permissionCategories: '权限分类',
|
|
93
94
|
selectAllPermissions: '全选',
|
|
94
95
|
selectVisiblePermissions: '选择“{{name}}”中显示的权限',
|
|
95
|
-
permissionSummary: '
|
|
96
|
-
noRecordScope: '
|
|
96
|
+
permissionSummary: '已授权 {{enabled}} / {{total}}',
|
|
97
|
+
noRecordScope: '不允许访问数据',
|
|
98
|
+
preservedRecordScope: '保留现有配置',
|
|
97
99
|
permissionFilter: {
|
|
98
100
|
limit: '最多支持 20 个条件',
|
|
99
|
-
configure: '
|
|
100
|
-
title: '
|
|
101
|
-
description: '
|
|
101
|
+
configure: '设置条件',
|
|
102
|
+
title: '设置数据范围',
|
|
103
|
+
description: '仅允许访问符合以下条件的数据,保存角色后生效。',
|
|
102
104
|
all: '满足全部条件',
|
|
103
105
|
any: '满足任一条件',
|
|
104
106
|
field: '字段',
|
|
@@ -5,6 +5,28 @@ 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
|
+
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
|
+
},
|
|
8
30
|
settings,
|
|
9
31
|
'auth.preparingSession': '正在准备登录会话…',
|
|
10
32
|
'auth.connectionFailed': '连接失败',
|
|
@@ -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,51 @@
|
|
|
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
|
+
const resources = new Map<string, CatalogResource>();
|
|
28
|
+
const visit = (items: readonly AppClientRegisteredRoute[]): void => {
|
|
29
|
+
for (const route of items) {
|
|
30
|
+
if (
|
|
31
|
+
route.componentLoader &&
|
|
32
|
+
typeof route.authz === 'object' &&
|
|
33
|
+
route.authz.resource.type === 'page'
|
|
34
|
+
) {
|
|
35
|
+
const id = route.authz.resource.id;
|
|
36
|
+
// Resolved child routes may inherit the parent's authorization resource.
|
|
37
|
+
// They share one grant, so showing independent checkboxes is misleading.
|
|
38
|
+
if (!resources.has(id))
|
|
39
|
+
resources.set(id, {
|
|
40
|
+
type: 'page',
|
|
41
|
+
id,
|
|
42
|
+
title: pagePermissionTitle(route, titles) ?? unnamed(route.path),
|
|
43
|
+
actions: [{ name: 'access', title: { key: 'settings.access' } }],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
visit(route.children ?? []);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
visit(routes);
|
|
50
|
+
return [...resources.values()];
|
|
51
|
+
}
|
|
@@ -17,6 +17,15 @@ export function setPermission(
|
|
|
17
17
|
action: string,
|
|
18
18
|
enabled: boolean,
|
|
19
19
|
): readonly PermissionGrant[] {
|
|
20
|
+
const existing =
|
|
21
|
+
grants
|
|
22
|
+
.find(
|
|
23
|
+
(grant) =>
|
|
24
|
+
grant.resource.type === resource.type &&
|
|
25
|
+
grant.resource.id === resource.id,
|
|
26
|
+
)
|
|
27
|
+
?.actions.some((entry) => entry.action === action) ?? false;
|
|
28
|
+
if (existing === enabled) return grants;
|
|
20
29
|
const next = grants.map((grant) => ({
|
|
21
30
|
...grant,
|
|
22
31
|
actions: [...grant.actions],
|
|
@@ -87,40 +96,55 @@ export function permissionSections(
|
|
|
87
96
|
return {
|
|
88
97
|
...row,
|
|
89
98
|
scopes: granted
|
|
90
|
-
? (resource.dataScopes?.[action.name] ?? []).map((scope) =>
|
|
91
|
-
|
|
92
|
-
resource.type,
|
|
93
|
-
resource.id,
|
|
94
|
-
action.name,
|
|
95
|
-
scope.key,
|
|
96
|
-
]),
|
|
97
|
-
label: text(
|
|
98
|
-
typeof scope.title === 'string' &&
|
|
99
|
-
scope.title === scope.collection
|
|
100
|
-
? { key: 'settings.recordScope' }
|
|
101
|
-
: scope.title,
|
|
102
|
-
),
|
|
103
|
-
levels: [
|
|
104
|
-
{
|
|
105
|
-
value: '',
|
|
106
|
-
label: text({ key: 'settings.noRecordScope' }),
|
|
107
|
-
},
|
|
108
|
-
...scope.recordAccess.map((key) => ({
|
|
109
|
-
value: key,
|
|
110
|
-
label: text(
|
|
111
|
-
catalog.recordAccess.find(
|
|
112
|
-
(entry) => entry.key === key,
|
|
113
|
-
)?.title ?? key,
|
|
114
|
-
),
|
|
115
|
-
})),
|
|
116
|
-
],
|
|
117
|
-
level: scopeSelectionKey(
|
|
99
|
+
? (resource.dataScopes?.[action.name] ?? []).map((scope) => {
|
|
100
|
+
const value = scopeSelectionKey(
|
|
118
101
|
granted.policy?.scopes &&
|
|
119
102
|
typeof granted.policy.scopes === 'object'
|
|
120
103
|
? Reflect.get(granted.policy.scopes, scope.key)
|
|
121
104
|
: undefined,
|
|
122
|
-
)
|
|
123
|
-
|
|
105
|
+
);
|
|
106
|
+
const preserved =
|
|
107
|
+
value !== '' && !scope.recordAccess.includes(value);
|
|
108
|
+
return {
|
|
109
|
+
id: JSON.stringify([
|
|
110
|
+
resource.type,
|
|
111
|
+
resource.id,
|
|
112
|
+
action.name,
|
|
113
|
+
scope.key,
|
|
114
|
+
]),
|
|
115
|
+
label: text(
|
|
116
|
+
typeof scope.title === 'string' &&
|
|
117
|
+
scope.title === scope.collection
|
|
118
|
+
? { key: 'settings.recordScope' }
|
|
119
|
+
: scope.title,
|
|
120
|
+
),
|
|
121
|
+
levels: [
|
|
122
|
+
...(preserved
|
|
123
|
+
? [
|
|
124
|
+
{
|
|
125
|
+
value,
|
|
126
|
+
label: text({
|
|
127
|
+
key: 'settings.preservedRecordScope',
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
: []),
|
|
132
|
+
{
|
|
133
|
+
value: '',
|
|
134
|
+
label: text({ key: 'settings.noRecordScope' }),
|
|
135
|
+
},
|
|
136
|
+
...scope.recordAccess.map((key) => ({
|
|
137
|
+
value: key,
|
|
138
|
+
label: text(
|
|
139
|
+
catalog.recordAccess.find(
|
|
140
|
+
(entry) => entry.key === key,
|
|
141
|
+
)?.title ?? key,
|
|
142
|
+
),
|
|
143
|
+
})),
|
|
144
|
+
],
|
|
145
|
+
level: value,
|
|
146
|
+
};
|
|
147
|
+
})
|
|
124
148
|
: [],
|
|
125
149
|
};
|
|
126
150
|
}),
|
|
@@ -136,6 +160,11 @@ export function setScope(
|
|
|
136
160
|
value: unknown,
|
|
137
161
|
): readonly PermissionGrant[] {
|
|
138
162
|
const [type, id, action, scope] = JSON.parse(rowId) as string[];
|
|
163
|
+
if (
|
|
164
|
+
typeof value === 'string' &&
|
|
165
|
+
scopeSelectionKey(getScopeSelection(grants, rowId)) === value
|
|
166
|
+
)
|
|
167
|
+
return grants;
|
|
139
168
|
return grants.map((grant) =>
|
|
140
169
|
grant.resource.type !== type || grant.resource.id !== id
|
|
141
170
|
? grant
|
|
@@ -216,7 +245,7 @@ function scopeSelectionKey(value: unknown): string {
|
|
|
216
245
|
typeof Reflect.get(value, 'key') === 'string'
|
|
217
246
|
)
|
|
218
247
|
return String(Reflect.get(value, 'key'));
|
|
219
|
-
return '';
|
|
248
|
+
return value == null || value === '' ? '' : '__preserved_scope__';
|
|
220
249
|
}
|
|
221
250
|
|
|
222
251
|
/** Legacy bare custom-filter keys require configuration before saving. */
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
type PermissionGrant,
|
|
17
17
|
type PermissionSet,
|
|
18
18
|
} from '@nocobase/app-plugin-authorization/client';
|
|
19
|
-
import
|
|
19
|
+
import { pagePermissionResources } from './page-permissions.js';
|
|
20
|
+
import { pagePermissionTitles } from './page-permission-titles.js';
|
|
20
21
|
import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
|
|
21
22
|
import {
|
|
22
23
|
useLocation,
|
|
@@ -40,7 +41,6 @@ import {
|
|
|
40
41
|
unconfiguredFilter,
|
|
41
42
|
setPermissions,
|
|
42
43
|
setScope,
|
|
43
|
-
type CatalogResource,
|
|
44
44
|
} from './permissions-model.js';
|
|
45
45
|
|
|
46
46
|
function RoleEditor(): ReactElement {
|
|
@@ -100,32 +100,11 @@ function RoleEditor(): ReactElement {
|
|
|
100
100
|
const catalog = useMemo(() => {
|
|
101
101
|
if (!options.data) return undefined;
|
|
102
102
|
const source = options.data.data;
|
|
103
|
-
const pages
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
route.componentLoader &&
|
|
109
|
-
typeof route.authz === 'object' &&
|
|
110
|
-
route.authz.resource.type === 'page'
|
|
111
|
-
)
|
|
112
|
-
pages.push({
|
|
113
|
-
type: 'page',
|
|
114
|
-
id: route.authz.resource.id,
|
|
115
|
-
title: pageTitle
|
|
116
|
-
? { key: pageTitle, ns: route.packageName }
|
|
117
|
-
: route.id,
|
|
118
|
-
actions: [
|
|
119
|
-
{
|
|
120
|
-
name: 'access',
|
|
121
|
-
title: { key: 'settings.access' },
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
});
|
|
125
|
-
collect(route.children ?? []);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
collect(app.runtime.routes);
|
|
103
|
+
const pages = pagePermissionResources(
|
|
104
|
+
app.runtime.routes,
|
|
105
|
+
pagePermissionTitles,
|
|
106
|
+
(path) => t('settings.unnamedPage', { path }),
|
|
107
|
+
);
|
|
129
108
|
return {
|
|
130
109
|
...source,
|
|
131
110
|
sections: [
|
|
@@ -180,7 +159,7 @@ function RoleEditor(): ReactElement {
|
|
|
180
159
|
})),
|
|
181
160
|
],
|
|
182
161
|
};
|
|
183
|
-
}, [options.data, app]);
|
|
162
|
+
}, [options.data, app, t]);
|
|
184
163
|
const allowed = creating
|
|
185
164
|
? canCreate
|
|
186
165
|
: canUpdate &&
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.71",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.71"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "2.0.0",
|
|
@@ -75,13 +75,15 @@
|
|
|
75
75
|
"vite": "^6.3.5",
|
|
76
76
|
"vitest": "^4.1.10",
|
|
77
77
|
"tar": "^7.5.22",
|
|
78
|
+
"@xyflow/react": "^12.11.5",
|
|
79
|
+
"elkjs": "^0.10.2",
|
|
78
80
|
"@nocobase/app-client": "^3.0.0-beta.2",
|
|
79
81
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
80
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
82
|
+
"@nocobase/app-skills": "0.1.0-beta.39",
|
|
81
83
|
"@nocobase/app-testing": "0.1.0-beta.4",
|
|
82
84
|
"@nocobase/app-websocket": "^0.0.2-beta.0",
|
|
83
|
-
"@nocobase/drive": "^0.1.0-beta.6",
|
|
84
85
|
"@nocobase/dev-config": "0.1.0-beta.23",
|
|
86
|
+
"@nocobase/drive": "^0.1.0-beta.7",
|
|
85
87
|
"@nocobase/logging": "^0.1.0-beta.7",
|
|
86
88
|
"@nocobase/session": "^0.1.0-beta.3"
|
|
87
89
|
},
|
|
@@ -146,19 +148,19 @@
|
|
|
146
148
|
"@nocobase/app-plugin-authentication": "^2.0.0-beta.10",
|
|
147
149
|
"@nocobase/app-plugin-authorization": "^1.0.0-beta.28",
|
|
148
150
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.15",
|
|
151
|
+
"@nocobase/app-plugin-file": "^1.0.0-beta.24",
|
|
149
152
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.16",
|
|
150
153
|
"@nocobase/app-plugin-mail": "^1.0.0-beta.14",
|
|
151
|
-
"@nocobase/app-plugin-file": "^1.0.0-beta.24",
|
|
152
154
|
"@nocobase/app-plugin-notification": "^1.0.0-beta.27",
|
|
153
155
|
"@nocobase/app-plugin-notification-in-app": "^1.0.0-beta.25",
|
|
154
156
|
"@nocobase/app-plugin-notification-providers": "^0.2.0-beta.12",
|
|
155
157
|
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.17",
|
|
156
158
|
"@nocobase/app-plugin-users": "^2.0.0-beta.5",
|
|
159
|
+
"@nocobase/app-server": "^2.0.0-beta.9",
|
|
157
160
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
158
|
-
"@nocobase/app-server": "^2.0.0-beta.8",
|
|
159
161
|
"@nocobase/db": "^1.0.0-beta.21",
|
|
160
|
-
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
161
162
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
163
|
+
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
162
164
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
163
165
|
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.13",
|
|
164
166
|
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.13",
|
|
@@ -168,9 +170,9 @@
|
|
|
168
170
|
"@nocobase/jobs": "^0.1.0-beta.3",
|
|
169
171
|
"@nocobase/lifecycle": "^0.1.0-beta.2",
|
|
170
172
|
"@nocobase/i18n": "^1.0.0-beta.7",
|
|
171
|
-
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
172
173
|
"@nocobase/caching": "^0.1.0-beta.3",
|
|
173
|
-
"@nocobase/
|
|
174
|
+
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
175
|
+
"@nocobase/app-cli": "^1.0.0-beta.24"
|
|
174
176
|
},
|
|
175
177
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
176
178
|
"imports": {
|
|
@@ -475,3 +475,65 @@ it('aligns semantic actions across resources without dropping special or filtere
|
|
|
475
475
|
view.container.querySelectorAll('[data-slot=permission-row]'),
|
|
476
476
|
).toHaveLength(1);
|
|
477
477
|
});
|
|
478
|
+
|
|
479
|
+
it('keeps scope captions consistent in a category with multiple data ranges, including when filtered', async () => {
|
|
480
|
+
const level = [{ value: 'all', label: 'All records' }];
|
|
481
|
+
render(
|
|
482
|
+
<PermissionEditor
|
|
483
|
+
sections={[
|
|
484
|
+
{
|
|
485
|
+
id: 'business',
|
|
486
|
+
title: 'Business',
|
|
487
|
+
groups: [
|
|
488
|
+
{
|
|
489
|
+
id: 'quotes',
|
|
490
|
+
title: 'Quotes',
|
|
491
|
+
rows: [
|
|
492
|
+
{
|
|
493
|
+
id: 'view',
|
|
494
|
+
label: 'View quotes',
|
|
495
|
+
enabled: true,
|
|
496
|
+
scopes: [
|
|
497
|
+
{
|
|
498
|
+
id: 'view-records',
|
|
499
|
+
label: 'Visible quotes',
|
|
500
|
+
level: 'all',
|
|
501
|
+
levels: level,
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: 'submit',
|
|
507
|
+
label: 'Submit quote',
|
|
508
|
+
enabled: true,
|
|
509
|
+
scopes: [
|
|
510
|
+
{
|
|
511
|
+
id: 'submit-projects',
|
|
512
|
+
label: 'Linked projects',
|
|
513
|
+
level: 'all',
|
|
514
|
+
levels: level,
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
id: 'submit-quotes',
|
|
518
|
+
label: 'Allowed quotes',
|
|
519
|
+
level: 'all',
|
|
520
|
+
levels: level,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
},
|
|
528
|
+
]}
|
|
529
|
+
onEnabledChange={vi.fn()}
|
|
530
|
+
onLevelChange={vi.fn()}
|
|
531
|
+
/>,
|
|
532
|
+
);
|
|
533
|
+
expect(screen.getByText('Visible quotes')).toBeVisible();
|
|
534
|
+
expect(screen.getByText('Linked projects')).toBeVisible();
|
|
535
|
+
expect(screen.getByText('Allowed quotes')).toBeVisible();
|
|
536
|
+
await userEvent.type(screen.getByRole('textbox'), 'View quotes');
|
|
537
|
+
expect(screen.queryByText('Linked projects')).not.toBeInTheDocument();
|
|
538
|
+
expect(screen.getByText('Visible quotes')).toBeVisible();
|
|
539
|
+
});
|