@gadmin2n/schematics 0.0.146 → 0.0.148

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 (95) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +70 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +1 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +17 -4
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -3
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +49 -11
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +1 -2
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +47 -3
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  93. package/package.json +1 -1
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -0,0 +1,214 @@
1
+ import {
2
+ DeleteOutlined,
3
+ EditOutlined,
4
+ PlusOutlined,
5
+ ReloadOutlined,
6
+ } from '@ant-design/icons';
7
+ import {
8
+ Button,
9
+ Card,
10
+ Input,
11
+ Popconfirm,
12
+ Select,
13
+ Space,
14
+ Table,
15
+ Tag,
16
+ Tooltip,
17
+ Typography,
18
+ message,
19
+ type TableColumnsType,
20
+ } from 'antd';
21
+ import { PageHeader } from 'components';
22
+ import { PAGE_WRAPPER_STYLE, SEARCH_CARD_PROPS } from 'constants/layout';
23
+ import { useCallback, useEffect, useState } from 'react';
24
+ import { PersonModal } from './PersonModal';
25
+ import {
26
+ deletePerson,
27
+ fetchPeople,
28
+ type PersonRecord,
29
+ } from './person-directory.api';
30
+
31
+ const PAGE_SIZE = 20;
32
+
33
+ export default function PersonDirectoryPage() {
34
+ const [people, setPeople] = useState<PersonRecord[]>([]);
35
+ const [total, setTotal] = useState(0);
36
+ const [page, setPage] = useState(1);
37
+ const [query, setQuery] = useState('');
38
+ const [active, setActive] = useState<boolean>();
39
+ const [loading, setLoading] = useState(false);
40
+ const [deletingId, setDeletingId] = useState<number>();
41
+ const [editing, setEditing] = useState<PersonRecord | null | undefined>();
42
+
43
+ const load = useCallback(async () => {
44
+ setLoading(true);
45
+ try {
46
+ const result = await fetchPeople({
47
+ query,
48
+ active,
49
+ page,
50
+ pageSize: PAGE_SIZE,
51
+ });
52
+ setPeople(result.data);
53
+ setTotal(result.meta.itemCount);
54
+ } catch (error: any) {
55
+ message.error(error?.message ?? '加载人员失败');
56
+ } finally {
57
+ setLoading(false);
58
+ }
59
+ }, [active, page, query]);
60
+
61
+ useEffect(() => void load(), [load]);
62
+
63
+ async function remove(person: PersonRecord) {
64
+ setDeletingId(person.id);
65
+ try {
66
+ await deletePerson(person.id);
67
+ message.success('人员已删除');
68
+ if (people.length === 1 && page > 1) setPage(page - 1);
69
+ else await load();
70
+ } catch (error: any) {
71
+ message.error(error?.message ?? '删除人员失败');
72
+ } finally {
73
+ setDeletingId(undefined);
74
+ }
75
+ }
76
+
77
+ const columns: TableColumnsType<PersonRecord> = [
78
+ {
79
+ title: '人员',
80
+ key: 'person',
81
+ render: (_, person) => (
82
+ <div>
83
+ <Typography.Text strong>
84
+ {person.username || person.userid}
85
+ </Typography.Text>
86
+ <br />
87
+ <Typography.Text type="secondary">{person.userid}</Typography.Text>
88
+ </div>
89
+ ),
90
+ },
91
+ {
92
+ title: '备注',
93
+ dataIndex: 'description',
94
+ render: (value?: string | null) => value || '—',
95
+ },
96
+ {
97
+ title: '状态',
98
+ dataIndex: 'isActive',
99
+ width: 100,
100
+ render: (value: boolean) => (
101
+ <Tag color={value ? 'green' : 'default'}>
102
+ {value ? '已启用' : '已停用'}
103
+ </Tag>
104
+ ),
105
+ },
106
+ {
107
+ title: '操作',
108
+ width: 110,
109
+ render: (_, person) => (
110
+ <Space size={4}>
111
+ <Tooltip title="编辑">
112
+ <Button
113
+ size="small"
114
+ icon={<EditOutlined />}
115
+ aria-label={`编辑 ${person.username || person.userid}`}
116
+ onClick={() => setEditing(person)}
117
+ />
118
+ </Tooltip>
119
+ <Popconfirm
120
+ title="删除该人员?"
121
+ description="只有未授予角色且未被人员组或 Workflow 引用的人员才能删除。"
122
+ okText="删除"
123
+ cancelText="取消"
124
+ okButtonProps={{ danger: true }}
125
+ onConfirm={() => remove(person)}
126
+ >
127
+ <Tooltip title="删除">
128
+ <Button
129
+ danger
130
+ size="small"
131
+ icon={<DeleteOutlined />}
132
+ loading={deletingId === person.id}
133
+ aria-label={`删除 ${person.username || person.userid}`}
134
+ />
135
+ </Tooltip>
136
+ </Popconfirm>
137
+ </Space>
138
+ ),
139
+ },
140
+ ];
141
+
142
+ return (
143
+ <div style={PAGE_WRAPPER_STYLE}>
144
+ <PageHeader
145
+ title="人员目录"
146
+ subtitle="维护可被审批、人员组等功能引用的人员;未授予角色的人员不能访问系统。"
147
+ extra={
148
+ <Space>
149
+ <Button icon={<ReloadOutlined />} onClick={load} />
150
+ <Button
151
+ type="primary"
152
+ icon={<PlusOutlined />}
153
+ onClick={() => setEditing(null)}
154
+ >
155
+ 添加人员
156
+ </Button>
157
+ </Space>
158
+ }
159
+ />
160
+ <Card {...SEARCH_CARD_PROPS}>
161
+ <Space wrap>
162
+ <Input.Search
163
+ allowClear
164
+ placeholder="搜索姓名或账号"
165
+ style={{ width: 260 }}
166
+ onSearch={(value) => {
167
+ setPage(1);
168
+ setQuery(value);
169
+ }}
170
+ />
171
+ <Select
172
+ allowClear
173
+ placeholder="全部状态"
174
+ style={{ width: 140 }}
175
+ options={[
176
+ { label: '已启用', value: true },
177
+ { label: '已停用', value: false },
178
+ ]}
179
+ onChange={(value) => {
180
+ setPage(1);
181
+ setActive(value);
182
+ }}
183
+ />
184
+ </Space>
185
+ </Card>
186
+ <Card className="card-table">
187
+ <Table
188
+ rowKey="id"
189
+ loading={loading}
190
+ dataSource={people}
191
+ columns={columns}
192
+ pagination={{
193
+ current: page,
194
+ pageSize: PAGE_SIZE,
195
+ total,
196
+ showTotal: (count) => `共 ${count} 人`,
197
+ onChange: setPage,
198
+ }}
199
+ />
200
+ </Card>
201
+ {editing !== undefined && (
202
+ <PersonModal
203
+ open
204
+ person={editing}
205
+ onCancel={() => setEditing(undefined)}
206
+ onSaved={() => {
207
+ setEditing(undefined);
208
+ void load();
209
+ }}
210
+ />
211
+ )}
212
+ </div>
213
+ );
214
+ }
@@ -0,0 +1,79 @@
1
+ import { customRequest } from 'helpers/http';
2
+
3
+ export interface PersonRecord {
4
+ id: number;
5
+ userid: string;
6
+ username: string | null;
7
+ description: string | null;
8
+ isActive: boolean;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ }
12
+
13
+ interface PersonPage {
14
+ data: PersonRecord[];
15
+ meta: { itemCount: number };
16
+ }
17
+
18
+ const personSelect = {
19
+ id: true,
20
+ userid: true,
21
+ username: true,
22
+ description: true,
23
+ isActive: true,
24
+ createdAt: true,
25
+ updatedAt: true,
26
+ };
27
+
28
+ export async function fetchPeople(input: {
29
+ query: string;
30
+ active?: boolean;
31
+ page: number;
32
+ pageSize: number;
33
+ }) {
34
+ const query = input.query.trim();
35
+ return customRequest<PersonPage>('user/findMany', 'POST', {
36
+ where: {
37
+ isApiKey: false,
38
+ ...(input.active !== undefined && { isActive: input.active }),
39
+ ...(query && {
40
+ OR: [
41
+ { userid: { contains: query, mode: 'insensitive' } },
42
+ { username: { contains: query, mode: 'insensitive' } },
43
+ ],
44
+ }),
45
+ },
46
+ select: personSelect,
47
+ orderBy: { userid: 'asc' },
48
+ skip: (input.page - 1) * input.pageSize,
49
+ take: input.pageSize,
50
+ });
51
+ }
52
+
53
+ export function createPerson(input: {
54
+ userid: string;
55
+ username?: string;
56
+ description?: string;
57
+ isActive: boolean;
58
+ }) {
59
+ return customRequest<PersonRecord>('user/createOne', 'POST', {
60
+ ...input,
61
+ roles: '[]',
62
+ isApiKey: false,
63
+ });
64
+ }
65
+
66
+ export function updatePerson(
67
+ id: number,
68
+ input: {
69
+ username?: string;
70
+ description?: string | null;
71
+ isActive: boolean;
72
+ },
73
+ ) {
74
+ return customRequest<PersonRecord>(`user/updateUnique/${id}`, 'PATCH', input);
75
+ }
76
+
77
+ export function deletePerson(id: number) {
78
+ return customRequest<{ count: number }>(`user/deletePerson/${id}`, 'DELETE');
79
+ }
@@ -98,16 +98,16 @@ export const UserFormModal: React.FC<Props> = ({
98
98
  //TODO: modalProps.onCancel expect an event so, I used close. Actually both of them are same.
99
99
  close();
100
100
  }}
101
- title={action === 'create' ? 'Create User' : 'Edit User'}
101
+ title={action === 'create' ? '新增访问主体' : '编辑访问授权'}
102
102
  >
103
103
  <Form {...formProps} layout="vertical">
104
104
  <Form.Item
105
105
  rules={[{ required: true }]}
106
106
  name="userid"
107
- label="User ID(WecomId 或者 太湖帐号名)"
107
+ label="账号(企业微信 ID 或太湖账号)"
108
108
  >
109
109
  <Input
110
- placeholder="Please enter user ID"
110
+ placeholder="请输入账号"
111
111
  onChange={(e) => {
112
112
  if (action === 'create' && !usernameTouched) {
113
113
  form.setFieldsValue({ username: e.target.value });
@@ -116,23 +116,19 @@ export const UserFormModal: React.FC<Props> = ({
116
116
  />
117
117
  </Form.Item>
118
118
 
119
- <Form.Item
120
- rules={[{ required: true }]}
121
- name="username"
122
- label="Username(一般设置为和上面的 User ID 一样)"
123
- >
119
+ <Form.Item rules={[{ required: true }]} name="username" label="名称">
124
120
  <Input
125
- placeholder="Please enter username"
121
+ placeholder="请输入名称"
126
122
  onChange={() => {
127
123
  if (!usernameTouched) setUsernameTouched(true);
128
124
  }}
129
125
  />
130
126
  </Form.Item>
131
127
 
132
- <Form.Item rules={[{ required: true }]} name="roles" label="Roles">
128
+ <Form.Item rules={[{ required: true }]} name="roles" label="角色">
133
129
  <Select
134
130
  mode="multiple"
135
- placeholder="Please select roles"
131
+ placeholder="请选择角色"
136
132
  options={roleOptions}
137
133
  loading={rolesLoading}
138
134
  />
@@ -137,7 +137,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
137
137
  icon={<PlusOutlined />}
138
138
  onClick={() => setShowModal({ type: 'create', id: null })}
139
139
  >
140
- Add User
140
+ 新增访问主体
141
141
  </Button>
142
142
  );
143
143
  }}
@@ -155,7 +155,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
155
155
  >
156
156
  <Table.Column
157
157
  dataIndex="userid"
158
- title="User ID"
158
+ title="账号"
159
159
  render={(_, record) => {
160
160
  return (
161
161
  <Space>
@@ -173,7 +173,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
173
173
  />
174
174
  <Table.Column
175
175
  dataIndex="username"
176
- title="User Name"
176
+ title="名称"
177
177
  filterDropdown={(props) => (
178
178
  <FilterDropdown
179
179
  {...props}
@@ -194,7 +194,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
194
194
  />
195
195
  <Table.Column
196
196
  dataIndex="roles"
197
- title="Roles"
197
+ title="角色"
198
198
  defaultFilteredValue={getDefaultFilter(
199
199
  'roles',
200
200
  filters,
@@ -224,7 +224,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
224
224
  />
225
225
  <Table.Column
226
226
  dataIndex="isApiKey"
227
- title="API Key"
227
+ title="API Key 主体"
228
228
  defaultFilteredValue={getDefaultFilter('isApiKey', filters, 'eq')}
229
229
  filterDropdown={(props) => (
230
230
  <FilterDropdown
@@ -258,7 +258,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
258
258
  />
259
259
  <Table.Column
260
260
  dataIndex="isActive"
261
- title="Active"
261
+ title="状态"
262
262
  defaultFilteredValue={getDefaultFilter('isActive', filters, 'eq')}
263
263
  filterDropdown={(props) => (
264
264
  <FilterDropdown
@@ -292,7 +292,7 @@ export const UserListPage: React.FC<PropsWithChildren> = ({ children }) => {
292
292
  />
293
293
  <Table.Column
294
294
  fixed="right"
295
- title="Actions"
295
+ title="操作"
296
296
  dataIndex="actions"
297
297
  render={(_, record) => {
298
298
  return (
@@ -83,6 +83,13 @@ describe('NodeOutputSummary', () => {
83
83
  type: 'object',
84
84
  properties: {
85
85
  operation: { type: 'string' },
86
+ table: {
87
+ type: 'object',
88
+ properties: {
89
+ schema: { type: 'string' },
90
+ name: { type: 'string' },
91
+ },
92
+ },
86
93
  primaryKey: { type: 'object' },
87
94
  before: { type: ['object', 'null'] },
88
95
  after: { type: ['object', 'null'] },
@@ -90,6 +97,7 @@ describe('NodeOutputSummary', () => {
90
97
  },
91
98
  required: [
92
99
  'operation',
100
+ 'table',
93
101
  'primaryKey',
94
102
  'before',
95
103
  'after',
@@ -102,8 +110,20 @@ describe('NodeOutputSummary', () => {
102
110
  render(<NodeOutputSummary node={event} nodeType={nodeType} />);
103
111
 
104
112
  expect(screen.getByText('input.operation')).toBeInTheDocument();
113
+ expect(screen.getByText('操作类型')).toBeInTheDocument();
114
+ expect(
115
+ screen.getByText(
116
+ '数据库操作类型:INSERT(新增)、UPDATE(更新)或 DELETE(删除)',
117
+ ),
118
+ ).toBeInTheDocument();
119
+ expect(screen.getByText('input.table')).toBeInTheDocument();
120
+ expect(screen.getByText('数据表')).toBeInTheDocument();
105
121
  expect(screen.getByText('input.primaryKey')).toBeInTheDocument();
122
+ expect(screen.getByText('主键')).toBeInTheDocument();
123
+ expect(screen.getByText('变更前的数据')).toBeInTheDocument();
124
+ expect(screen.getByText('变更后的数据')).toBeInTheDocument();
106
125
  expect(screen.getByText('input.changedFields')).toBeInTheDocument();
126
+ expect(screen.getByText('发生变化的字段')).toBeInTheDocument();
107
127
  expect(
108
128
  screen.queryByText('触发本次运行的事件数据'),
109
129
  ).not.toBeInTheDocument();
@@ -188,11 +188,10 @@ describe('NodePropertyPanel instance references', () => {
188
188
  expect(
189
189
  screen.queryByText('Connection is required'),
190
190
  ).not.toBeInTheDocument();
191
- await user.click(screen.getByRole('button', { name: 'Save changes' }));
192
- expect(onSave).toHaveBeenCalledWith(
193
- 'http-1',
194
- expect.objectContaining({ config: expect.any(Object) }),
195
- );
191
+ expect(
192
+ screen.queryByRole('button', { name: 'Save changes' }),
193
+ ).not.toBeInTheDocument();
194
+ expect(onSave).not.toHaveBeenCalled();
196
195
  expect(
197
196
  screen.queryByText('Connection is required'),
198
197
  ).not.toBeInTheDocument();
@@ -307,7 +306,6 @@ describe('NodePropertyPanel instance references', () => {
307
306
  />,
308
307
  );
309
308
 
310
- await waitFor(() => expect(customRequest).toHaveBeenCalled());
311
309
  await act(async () => {
312
310
  await Promise.resolve();
313
311
  await Promise.resolve();
@@ -346,8 +344,8 @@ describe('NodePropertyPanel instance references', () => {
346
344
  screen.getByText('Includes the approver and completion time'),
347
345
  ).toBeInTheDocument();
348
346
  expect(
349
- screen.getByRole('button', { name: 'Save changes' }),
350
- ).toBeInTheDocument();
347
+ screen.queryByRole('button', { name: 'Save changes' }),
348
+ ).not.toBeInTheDocument();
351
349
  expect(customRequest).not.toHaveBeenCalledWith(
352
350
  'workflowNodeInstance/findMany',
353
351
  expect.anything(),
@@ -386,13 +384,14 @@ describe('NodePropertyPanel instance references', () => {
386
384
  />,
387
385
  );
388
386
 
389
- expect(screen.getByRole('button', { name: 'Save changes' })).toBeEnabled();
390
387
  expect(
391
388
  screen.getByText('Use a template or configure this node directly'),
392
389
  ).toBeInTheDocument();
393
- expect(screen.getByRole('button', { name: /New template/ })).toHaveClass(
394
- 'workflow-panel-action',
395
- );
390
+ expect(
391
+ screen.getByRole('button', {
392
+ name: /Save current configuration as template/,
393
+ }),
394
+ ).toHaveClass('workflow-panel-action');
396
395
  expect(screen.getByText('Who can approve')).toBeInTheDocument();
397
396
  expect(screen.queryByText('Parameters')).not.toBeInTheDocument();
398
397
  expect(
@@ -419,7 +418,83 @@ describe('NodePropertyPanel instance references', () => {
419
418
  );
420
419
  });
421
420
 
422
- it('keeps an inline approval configuration after saving it', async () => {
421
+ it('saves the current configuration as a template and uses it immediately', async () => {
422
+ const user = userEvent.setup();
423
+ const onSave = vi.fn();
424
+ const inlineNode = {
425
+ ...node,
426
+ instanceRef: undefined,
427
+ config: { subjects: [{ refs: ['alice'] }] },
428
+ };
429
+ vi.mocked(customRequest).mockImplementation((path) => {
430
+ if (path === 'workflowNodeInstance/findMany') {
431
+ return Promise.resolve({ data: [] }) as never;
432
+ }
433
+ if (path === 'workflowNodeInstance/createOne') {
434
+ return Promise.resolve({
435
+ id: '21',
436
+ code: 'FINANCE_APPROVAL_V2',
437
+ name: 'Finance approval',
438
+ nodeTypeId: '7',
439
+ isEnabled: true,
440
+ config: inlineNode.config,
441
+ }) as never;
442
+ }
443
+ return Promise.resolve([]) as never;
444
+ });
445
+
446
+ render(
447
+ <NodePropertyPanel
448
+ node={inlineNode}
449
+ nodeTypes={[nodeType]}
450
+ onSave={onSave}
451
+ onLabelChange={vi.fn()}
452
+ onAiEdit={vi.fn()}
453
+ aiLoading={false}
454
+ />,
455
+ );
456
+
457
+ await user.click(
458
+ screen.getByRole('button', {
459
+ name: /Save current configuration as template/,
460
+ }),
461
+ );
462
+ expect(
463
+ screen.getByText(/Create a template from the current node configuration/),
464
+ ).toBeInTheDocument();
465
+ const templateName = screen.getByRole('textbox', {
466
+ name: 'Template name',
467
+ });
468
+ await user.clear(templateName);
469
+ await user.type(templateName, 'Reusable finance approval');
470
+ await user.click(
471
+ screen.getByRole('button', { name: 'Save and use template' }),
472
+ );
473
+
474
+ await waitFor(() =>
475
+ expect(customRequest).toHaveBeenCalledWith(
476
+ 'workflowNodeInstance/createOne',
477
+ 'POST',
478
+ expect.objectContaining({
479
+ name: 'Reusable finance approval',
480
+ config: inlineNode.config,
481
+ }),
482
+ ),
483
+ );
484
+ expect(onSave).toHaveBeenCalledWith(
485
+ 'node-1',
486
+ expect.objectContaining({
487
+ config: inlineNode.config,
488
+ configOverride: null,
489
+ instanceRef: {
490
+ instanceId: '21',
491
+ instanceCode: 'FINANCE_APPROVAL_V2',
492
+ },
493
+ }),
494
+ );
495
+ });
496
+
497
+ it('keeps an inline approval configuration in the Workflow draft', async () => {
423
498
  const user = userEvent.setup();
424
499
  const onSave = vi.fn();
425
500
  const onConfigDraftChange = vi.fn();
@@ -531,10 +606,8 @@ describe('NodePropertyPanel instance references', () => {
531
606
  },
532
607
  });
533
608
 
534
- await user.click(screen.getByRole('button', { name: 'Save changes' }));
535
-
536
- const savedConfig = onSave.mock.lastCall?.[1].config;
537
- expect(savedConfig).toMatchObject({
609
+ const pendingConfig = panelRef.current?.getPendingUpdate()?.updates.config;
610
+ expect(pendingConfig).toMatchObject({
538
611
  external: {
539
612
  request: {
540
613
  connectionRef: 'ticketing',
@@ -549,6 +622,7 @@ describe('NodePropertyPanel instance references', () => {
549
622
  rejectionRule: { mode: 'ANY' },
550
623
  timeout: { durationSeconds: 0, action: 'REJECTED' },
551
624
  });
625
+ expect(onSave).not.toHaveBeenCalled();
552
626
 
553
627
  await waitFor(() =>
554
628
  expect(screen.getByLabelText('Task title field path')).toHaveValue(
@@ -616,7 +690,7 @@ describe('NodePropertyPanel instance references', () => {
616
690
 
617
691
  it('uses nested UI Schema widgets for Callable Trigger parameters', async () => {
618
692
  const user = userEvent.setup();
619
- const onSave = vi.fn();
693
+ const onConfigDraftChange = vi.fn();
620
694
  const callableNodeType = {
621
695
  id: '9',
622
696
  type: 'callable_trigger',
@@ -655,7 +729,8 @@ describe('NodePropertyPanel instance references', () => {
655
729
  <NodePropertyPanel
656
730
  node={callableNode}
657
731
  nodeTypes={[callableNodeType]}
658
- onSave={onSave}
732
+ onSave={vi.fn()}
733
+ onConfigDraftChange={onConfigDraftChange}
659
734
  onLabelChange={vi.fn()}
660
735
  onAiEdit={vi.fn()}
661
736
  aiLoading={false}
@@ -672,16 +747,10 @@ describe('NodePropertyPanel instance references', () => {
672
747
  screen.getByRole('textbox', { name: 'Parameter name' }),
673
748
  'departmentId',
674
749
  );
675
- await user.click(screen.getByRole('button', { name: 'Save changes' }));
676
750
 
677
- expect(onSave).toHaveBeenCalledWith(
678
- 'callable-1',
679
- expect.objectContaining({
680
- config: {
681
- inputParameters: [{ key: 'departmentId', label: '', required: true }],
682
- },
683
- }),
684
- );
751
+ expect(onConfigDraftChange).toHaveBeenCalledWith('callable-1', {
752
+ inputParameters: [{ key: 'departmentId', label: '', required: true }],
753
+ });
685
754
  });
686
755
 
687
756
  it('filters the node palette by translated name and technical type', async () => {