@gadmin2n/schematics 0.0.147 → 0.0.149

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 (106) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +2 -1
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +83 -53
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +4 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +106 -37
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +59 -3
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +60 -14
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +60 -14
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +124 -62
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +78 -17
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  104. package/package.json +1 -1
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -0,0 +1,112 @@
1
+ import { Form, Input, Modal, Switch, message } from 'antd';
2
+ import { StaffSelect } from 'components';
3
+ import { useEffect, useState } from 'react';
4
+ import {
5
+ createPerson,
6
+ type PersonRecord,
7
+ updatePerson,
8
+ } from './person-directory.api';
9
+
10
+ interface Props {
11
+ open: boolean;
12
+ person?: PersonRecord | null;
13
+ onCancel(): void;
14
+ onSaved(): void;
15
+ }
16
+
17
+ export function PersonModal({ open, person, onCancel, onSaved }: Props) {
18
+ const [form] = Form.useForm();
19
+ const [saving, setSaving] = useState(false);
20
+
21
+ useEffect(() => {
22
+ if (!open) return;
23
+ form.resetFields();
24
+ form.setFieldsValue(
25
+ person
26
+ ? {
27
+ userid: person.userid,
28
+ username: person.username ?? '',
29
+ description: person.description ?? '',
30
+ isActive: person.isActive,
31
+ }
32
+ : {
33
+ userid: undefined,
34
+ username: '',
35
+ description: '',
36
+ isActive: true,
37
+ },
38
+ );
39
+ }, [form, open, person]);
40
+
41
+ async function save() {
42
+ try {
43
+ const values = await form.validateFields();
44
+ setSaving(true);
45
+ if (person) {
46
+ await updatePerson(person.id, {
47
+ username: values.username.trim() || undefined,
48
+ description: values.description.trim() || null,
49
+ isActive: values.isActive,
50
+ });
51
+ } else {
52
+ await createPerson({
53
+ userid: values.userid.trim(),
54
+ username: values.username.trim() || values.userid.trim(),
55
+ description: values.description.trim() || undefined,
56
+ isActive: values.isActive,
57
+ });
58
+ }
59
+ message.success(person ? '人员信息已更新' : '人员已添加');
60
+ onSaved();
61
+ } catch (error: any) {
62
+ if (error?.errorFields) return;
63
+ message.error(error?.message ?? '保存人员失败');
64
+ } finally {
65
+ setSaving(false);
66
+ }
67
+ }
68
+
69
+ return (
70
+ <Modal
71
+ open={open}
72
+ title={person ? '编辑人员' : '添加人员'}
73
+ okText="保存"
74
+ cancelText="取消"
75
+ confirmLoading={saving}
76
+ onOk={save}
77
+ onCancel={onCancel}
78
+ destroyOnClose
79
+ >
80
+ <Form form={form} layout="vertical" preserve={false}>
81
+ {person ? (
82
+ <Form.Item name="userid" label="账号">
83
+ <Input disabled />
84
+ </Form.Item>
85
+ ) : (
86
+ <Form.Item
87
+ name="userid"
88
+ label="人员"
89
+ rules={[{ required: true, message: '请选择人员' }]}
90
+ >
91
+ <StaffSelect />
92
+ </Form.Item>
93
+ )}
94
+ {person ? (
95
+ <Form.Item name="username" label="姓名">
96
+ <Input maxLength={128} placeholder="未填写时默认使用账号" />
97
+ </Form.Item>
98
+ ) : (
99
+ <Form.Item name="username" hidden>
100
+ <Input />
101
+ </Form.Item>
102
+ )}
103
+ <Form.Item name="description" label="备注">
104
+ <Input.TextArea rows={3} maxLength={512} />
105
+ </Form.Item>
106
+ <Form.Item name="isActive" label="启用" valuePropName="checked">
107
+ <Switch />
108
+ </Form.Item>
109
+ </Form>
110
+ </Modal>
111
+ );
112
+ }
@@ -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 (
@@ -60,9 +60,9 @@ describe('RegisterBusinessEventModal', () => {
60
60
  const sourceOptions = await screen.findAllByText('public.t_hardware_asset');
61
61
  fireEvent.click(sourceOptions[sourceOptions.length - 1]);
62
62
  await waitFor(() =>
63
- expect(screen.getByLabelText('监听字段')).not.toBeDisabled(),
63
+ expect(screen.getByLabelText('修改时监听字段')).not.toBeDisabled(),
64
64
  );
65
- fireEvent.mouseDown(screen.getByLabelText('监听字段'));
65
+ fireEvent.mouseDown(screen.getByLabelText('修改时监听字段'));
66
66
  const fieldOptions = await screen.findAllByText(/status/);
67
67
  fireEvent.click(fieldOptions[fieldOptions.length - 1]);
68
68
  fireEvent.click(screen.getByRole('button', { name: '完成登记' }));
@@ -71,7 +71,7 @@ describe('RegisterBusinessEventModal', () => {
71
71
  expect(apiMocks.register).toHaveBeenCalledWith({
72
72
  schemaName: 'public',
73
73
  tableName: 't_hardware_asset',
74
- operation: 'UPDATE',
74
+ operations: ['UPDATE'],
75
75
  watchedFields: ['status'],
76
76
  }),
77
77
  );
@@ -95,7 +95,16 @@ describe('RegisterBusinessEventModal', () => {
95
95
  const insertOptions = await screen.findAllByText('INSERT');
96
96
  fireEvent.click(insertOptions[insertOptions.length - 1]);
97
97
 
98
- expect(screen.queryByLabelText('监听字段')).not.toBeInTheDocument();
98
+ // The operation picker is multi-select: remove UPDATE for an INSERT-only registration.
99
+ fireEvent.click(
100
+ screen
101
+ .getAllByTitle('UPDATE')
102
+ .find((element) =>
103
+ element.classList.contains('ant-select-selection-item'),
104
+ )!
105
+ .querySelector('.ant-select-selection-item-remove')!,
106
+ );
107
+ expect(screen.queryByLabelText('修改时监听字段')).not.toBeInTheDocument();
99
108
  expect(screen.getByText(/任意一行插入或删除时触发/)).toBeInTheDocument();
100
109
  fireEvent.click(screen.getByRole('button', { name: '完成登记' }));
101
110
 
@@ -103,9 +112,41 @@ describe('RegisterBusinessEventModal', () => {
103
112
  expect(apiMocks.register).toHaveBeenCalledWith({
104
113
  schemaName: 'public',
105
114
  tableName: 't_hardware_asset',
106
- operation: 'INSERT',
115
+ operations: ['INSERT'],
107
116
  watchedFields: [],
108
117
  }),
109
118
  );
110
119
  });
120
+
121
+ it('registers INSERT and UPDATE together and still requires update fields', async () => {
122
+ await i18n.changeLanguage('zh_CN');
123
+ render(
124
+ <RegisterBusinessEventModal
125
+ open
126
+ onCancel={vi.fn()}
127
+ onRegistered={vi.fn()}
128
+ />,
129
+ );
130
+ fireEvent.mouseDown(await screen.findByLabelText('数据库表'));
131
+ const sourceOptions = await screen.findAllByText('public.t_hardware_asset');
132
+ fireEvent.click(sourceOptions[sourceOptions.length - 1]);
133
+ fireEvent.mouseDown(screen.getByLabelText('数据操作'));
134
+ const insertOptions = await screen.findAllByText('INSERT');
135
+ fireEvent.click(insertOptions[insertOptions.length - 1]);
136
+ fireEvent.click(screen.getByRole('button', { name: '完成登记' }));
137
+ await screen.findByRole('button', { name: '完成登记' });
138
+ expect(apiMocks.register).not.toHaveBeenCalled();
139
+ fireEvent.mouseDown(screen.getByLabelText('修改时监听字段'));
140
+ const options = await screen.findAllByText(/status/);
141
+ fireEvent.click(options[options.length - 1]);
142
+ fireEvent.click(await screen.findByRole('button', { name: '完成登记' }));
143
+ await waitFor(() =>
144
+ expect(apiMocks.register).toHaveBeenCalledWith({
145
+ schemaName: 'public',
146
+ tableName: 't_hardware_asset',
147
+ operations: ['UPDATE', 'INSERT'],
148
+ watchedFields: ['status'],
149
+ }),
150
+ );
151
+ });
111
152
  });
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
4
4
  import {
5
5
  eventCatalogApi,
6
6
  type BusinessEventSource,
7
+ type BusinessEventOperation,
7
8
  } from '../node-instances/widgets/event-catalog.api';
8
9
  import { useWorkflowFormValidation } from '../useWorkflowFormValidation';
9
10
 
@@ -15,7 +16,7 @@ interface RegisterBusinessEventModalProps {
15
16
 
16
17
  interface RegisterBusinessEventFormValues {
17
18
  source: string;
18
- operation: 'INSERT' | 'UPDATE' | 'DELETE';
19
+ operations: BusinessEventOperation[];
19
20
  watchedFields: string[];
20
21
  }
21
22
 
@@ -31,7 +32,7 @@ export function RegisterBusinessEventModal({
31
32
  const [loadingSources, setLoadingSources] = useState(false);
32
33
  const [submitting, setSubmitting] = useState(false);
33
34
  const sourceKey = Form.useWatch('source', form);
34
- const operation = Form.useWatch('operation', form);
35
+ const operations = Form.useWatch('operations', form) ?? [];
35
36
  const selectedSource = useMemo(
36
37
  () =>
37
38
  sources.find(
@@ -65,9 +66,10 @@ export function RegisterBusinessEventModal({
65
66
  await eventCatalogApi.register({
66
67
  schemaName: source.schemaName,
67
68
  tableName: source.tableName,
68
- operation: values.operation,
69
- watchedFields:
70
- values.operation === 'UPDATE' ? values.watchedFields : [],
69
+ operations: values.operations,
70
+ watchedFields: values.operations.includes('UPDATE')
71
+ ? values.watchedFields
72
+ : [],
71
73
  });
72
74
  message.success(t('eventCatalog.registration.success'));
73
75
  form.resetFields();
@@ -103,7 +105,7 @@ export function RegisterBusinessEventModal({
103
105
  form={form}
104
106
  layout="vertical"
105
107
  requiredMark={false}
106
- initialValues={{ operation: 'UPDATE', watchedFields: [] }}
108
+ initialValues={{ operations: ['UPDATE'], watchedFields: [] }}
107
109
  {...formValidation}
108
110
  >
109
111
  <Form.Item
@@ -123,21 +125,23 @@ export function RegisterBusinessEventModal({
123
125
  />
124
126
  </Form.Item>
125
127
  <Form.Item
126
- name="operation"
128
+ name="operations"
127
129
  label={t('eventCatalog.registration.operation')}
128
- rules={[{ required: true }]}
130
+ rules={[{ required: true, type: 'array', min: 1 }]}
129
131
  >
130
132
  <Select
133
+ mode="multiple"
131
134
  options={['INSERT', 'UPDATE', 'DELETE'].map((operation) => ({
132
135
  value: operation,
133
136
  label: operation,
134
137
  }))}
135
138
  onChange={(value) => {
136
- if (value !== 'UPDATE') form.setFieldValue('watchedFields', []);
139
+ if (!value.includes('UPDATE'))
140
+ form.setFieldValue('watchedFields', []);
137
141
  }}
138
142
  />
139
143
  </Form.Item>
140
- {operation === 'UPDATE' ? (
144
+ {operations.includes('UPDATE') ? (
141
145
  <Form.Item
142
146
  name="watchedFields"
143
147
  label={t('eventCatalog.registration.watchedFields')}
@@ -33,7 +33,7 @@ describe('BusinessEventDetailPage', () => {
33
33
  label: 'public.t_hardware_asset · UPDATE',
34
34
  schemaName: 'public',
35
35
  tableName: 't_hardware_asset',
36
- operation: 'UPDATE',
36
+ operations: ['UPDATE'],
37
37
  watchedFields: ['status'],
38
38
  primaryKeyFields: ['id'],
39
39
  isEnabled: true,