@gadmin2n/schematics 0.0.140 → 0.0.142

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 (46) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Event.ts +1 -1
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +1 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/gen-jwt.ts +8 -1
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +32 -4
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +39 -33
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.spec.ts +34 -1
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +26 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +149 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +20 -6
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.spec.ts +85 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.ts +157 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +59 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -4
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +125 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +109 -2
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/PageHeader.tsx +46 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -4
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +30 -2
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +20 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +20 -1
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +9 -20
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/audit/list.tsx +141 -138
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +8 -19
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/human-approval/inbox.tsx +12 -19
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +47 -150
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +13 -12
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +110 -113
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +61 -64
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +187 -200
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/index.tsx +46 -44
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NewNodeDefaults.test.tsx +6 -2
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +50 -41
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +9 -22
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +4 -8
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/index.tsx +9 -22
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalRequestConfigEditor.test.tsx +42 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalRequestConfigEditor.tsx +77 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.test.tsx +29 -1
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.tsx +245 -55
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +152 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +417 -161
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +5 -1
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/index.css +20 -0
  46. package/package.json +1 -1
@@ -1,12 +1,23 @@
1
- import { Input, Radio, Space, Typography } from 'antd';
1
+ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
2
+ import {
3
+ Button,
4
+ Card,
5
+ Input,
6
+ InputNumber,
7
+ Radio,
8
+ Space,
9
+ Typography,
10
+ } from 'antd';
2
11
  import { useEffect } from 'react';
3
12
  import { useTranslation } from 'react-i18next';
4
13
  import type { WidgetProps } from '../schema-form/widget-registry';
5
14
 
6
15
  type ReturnResultConfig = {
7
- resultType?: 'APPROVAL_SUBJECTS';
16
+ resultType?: 'JSON' | 'APPROVAL_SUBJECTS';
8
17
  sourceMode?: 'INPUT' | 'WORKFLOW';
9
18
  path?: string;
19
+ httpStatusCode?: number;
20
+ httpHeaders?: Record<string, string>;
10
21
  };
11
22
 
12
23
  export function ReturnResultConfigEditor({
@@ -15,77 +26,256 @@ export function ReturnResultConfigEditor({
15
26
  disabled,
16
27
  }: WidgetProps<ReturnResultConfig | undefined>) {
17
28
  const { t } = useTranslation('workflow');
29
+ const resultType =
30
+ value?.resultType === 'APPROVAL_SUBJECTS' ? 'APPROVAL_SUBJECTS' : 'JSON';
18
31
  const sourceMode =
19
32
  value?.sourceMode === 'INPUT' || value?.sourceMode === 'WORKFLOW'
20
33
  ? value.sourceMode
21
34
  : 'INPUT';
22
35
 
23
36
  useEffect(() => {
24
- if (value?.sourceMode !== 'INPUT' && value?.sourceMode !== 'WORKFLOW') {
37
+ if (
38
+ (value?.resultType !== 'JSON' &&
39
+ value?.resultType !== 'APPROVAL_SUBJECTS') ||
40
+ (value?.sourceMode !== 'INPUT' && value?.sourceMode !== 'WORKFLOW')
41
+ ) {
25
42
  onChange({
26
- resultType: 'APPROVAL_SUBJECTS',
43
+ resultType: 'JSON',
27
44
  sourceMode: 'INPUT',
45
+ httpStatusCode: 200,
46
+ httpHeaders: {},
28
47
  });
29
48
  }
30
- }, [onChange, value?.sourceMode]);
49
+ }, [onChange, value?.resultType, value?.sourceMode]);
50
+
51
+ function nextConfig(
52
+ nextResultType: 'JSON' | 'APPROVAL_SUBJECTS',
53
+ nextSourceMode: 'INPUT' | 'WORKFLOW',
54
+ patch: Partial<ReturnResultConfig> = {},
55
+ ): ReturnResultConfig {
56
+ return {
57
+ resultType: nextResultType,
58
+ sourceMode: nextSourceMode,
59
+ ...(nextSourceMode === 'WORKFLOW' && {
60
+ path: patch.path ?? value?.path ?? '',
61
+ }),
62
+ ...(nextResultType === 'JSON' && {
63
+ httpStatusCode: patch.httpStatusCode ?? value?.httpStatusCode ?? 200,
64
+ httpHeaders: patch.httpHeaders ?? value?.httpHeaders ?? {},
65
+ }),
66
+ };
67
+ }
31
68
 
32
69
  return (
33
- <Space direction="vertical" size={12} style={{ width: '100%' }}>
34
- <Typography.Text strong>{t('returnResult.source')}</Typography.Text>
35
- <Radio.Group
36
- value={sourceMode}
37
- disabled={disabled}
38
- onChange={(event) => {
39
- const nextMode = event.target.value as 'INPUT' | 'WORKFLOW';
40
- onChange(
41
- nextMode === 'INPUT'
42
- ? {
43
- resultType: 'APPROVAL_SUBJECTS',
44
- sourceMode: 'INPUT',
45
- }
46
- : {
47
- resultType: 'APPROVAL_SUBJECTS',
48
- sourceMode: 'WORKFLOW',
49
- path: value?.path ?? '',
50
- },
51
- );
52
- }}
53
- >
54
- <Space direction="vertical">
55
- <Radio value="INPUT">{t('returnResult.previousNode')}</Radio>
56
- <Radio value="WORKFLOW">{t('returnResult.workflowData')}</Radio>
57
- </Space>
58
- </Radio.Group>
59
- {sourceMode === 'INPUT' ? (
60
- <Typography.Text type="secondary">
61
- {t('returnResult.previousNodeHint')}
62
- </Typography.Text>
63
- ) : (
64
- <div>
65
- <Typography.Text>{t('returnResult.workflowPath')}</Typography.Text>
70
+ <Space direction="vertical" size={16} style={{ width: '100%' }}>
71
+ <Card size="small" title={t('returnResult.type')}>
72
+ <Radio.Group
73
+ value={resultType}
74
+ disabled={disabled}
75
+ onChange={(event) =>
76
+ onChange(
77
+ nextConfig(
78
+ event.target.value as 'JSON' | 'APPROVAL_SUBJECTS',
79
+ sourceMode,
80
+ ),
81
+ )
82
+ }
83
+ >
84
+ <Space direction="vertical">
85
+ <Radio value="JSON">{t('returnResult.ordinaryData')}</Radio>
86
+ <Radio value="APPROVAL_SUBJECTS">
87
+ <Space direction="vertical" size={0}>
88
+ <Typography.Text>
89
+ {t('returnResult.approvalSubjects')}
90
+ </Typography.Text>
91
+ <Typography.Text type="secondary">
92
+ {t('returnResult.approvalSubjectsHint')}
93
+ </Typography.Text>
94
+ </Space>
95
+ </Radio>
96
+ </Space>
97
+ </Radio.Group>
98
+ </Card>
99
+
100
+ <Card size="small" title={t('returnResult.source')}>
101
+ <Radio.Group
102
+ value={sourceMode}
103
+ disabled={disabled}
104
+ onChange={(event) =>
105
+ onChange(
106
+ nextConfig(
107
+ resultType,
108
+ event.target.value as 'INPUT' | 'WORKFLOW',
109
+ ),
110
+ )
111
+ }
112
+ >
113
+ <Space direction="vertical">
114
+ <Radio value="INPUT">{t('returnResult.previousNode')}</Radio>
115
+ <Radio value="WORKFLOW">{t('returnResult.workflowData')}</Radio>
116
+ </Space>
117
+ </Radio.Group>
118
+ {sourceMode === 'INPUT' ? (
119
+ <Typography.Text
120
+ type="secondary"
121
+ style={{ display: 'block', marginTop: 10 }}
122
+ >
123
+ {t('returnResult.previousNodeHint')}
124
+ </Typography.Text>
125
+ ) : (
126
+ <div style={{ marginTop: 12 }}>
127
+ <Typography.Text>{t('returnResult.workflowPath')}</Typography.Text>
128
+ <Input
129
+ aria-label={t('returnResult.workflowPath')}
130
+ addonBefore="workflow."
131
+ value={value?.path ?? ''}
132
+ disabled={disabled}
133
+ placeholder={resultType === 'JSON' ? 'body' : 'approvers'}
134
+ onChange={(event) =>
135
+ onChange(
136
+ nextConfig(resultType, 'WORKFLOW', {
137
+ path: event.target.value,
138
+ }),
139
+ )
140
+ }
141
+ style={{ marginTop: 6 }}
142
+ />
143
+ <Typography.Text
144
+ type="secondary"
145
+ style={{ display: 'block', marginTop: 6, fontSize: 12 }}
146
+ >
147
+ {t('returnResult.workflowHint')}
148
+ </Typography.Text>
149
+ </div>
150
+ )}
151
+ </Card>
152
+
153
+ {resultType === 'JSON' && (
154
+ <Card size="small" title={t('returnResult.httpResponse')}>
155
+ <Typography.Text>{t('returnResult.httpStatusCode')}</Typography.Text>
156
+ <InputNumber
157
+ aria-label={t('returnResult.httpStatusCode')}
158
+ min={100}
159
+ max={599}
160
+ precision={0}
161
+ value={value?.httpStatusCode ?? 200}
162
+ disabled={disabled}
163
+ onChange={(next) =>
164
+ onChange(
165
+ nextConfig('JSON', sourceMode, {
166
+ httpStatusCode: next ?? 200,
167
+ }),
168
+ )
169
+ }
170
+ style={{ display: 'block', marginTop: 6, width: '100%' }}
171
+ />
172
+ <ResponseHeadersEditor
173
+ value={value?.httpHeaders ?? {}}
174
+ disabled={disabled}
175
+ onChange={(httpHeaders) =>
176
+ onChange(nextConfig('JSON', sourceMode, { httpHeaders }))
177
+ }
178
+ />
179
+ </Card>
180
+ )}
181
+ </Space>
182
+ );
183
+ }
184
+
185
+ function ResponseHeadersEditor({
186
+ value,
187
+ disabled,
188
+ onChange,
189
+ }: {
190
+ value: Record<string, string>;
191
+ disabled?: boolean;
192
+ onChange(value: Record<string, string>): void;
193
+ }) {
194
+ const { t } = useTranslation('workflow');
195
+ const entries = Object.entries(value);
196
+
197
+ function replace(index: number, key: string, headerValue: string) {
198
+ onChange(
199
+ Object.fromEntries(
200
+ entries.map((entry, current) =>
201
+ current === index ? [key, headerValue] : entry,
202
+ ),
203
+ ),
204
+ );
205
+ }
206
+
207
+ return (
208
+ <Space
209
+ direction="vertical"
210
+ size={8}
211
+ style={{ width: '100%', marginTop: 16 }}
212
+ >
213
+ <Typography.Text>{t('returnResult.httpHeaders')}</Typography.Text>
214
+ {entries.map(([key, headerValue], index) => (
215
+ <div
216
+ key={`${key}-${index}`}
217
+ style={{
218
+ display: 'grid',
219
+ gridTemplateColumns: 'minmax(120px, 0.8fr) minmax(160px, 1fr) 32px',
220
+ gap: 8,
221
+ }}
222
+ >
66
223
  <Input
67
- aria-label={t('returnResult.workflowPath')}
68
- addonBefore="workflow."
69
- value={value?.path ?? ''}
224
+ aria-label={t('returnResult.headerName')}
225
+ value={key}
70
226
  disabled={disabled}
71
- placeholder="approvers"
227
+ placeholder="X-Result-Type"
72
228
  onChange={(event) =>
73
- onChange({
74
- resultType: 'APPROVAL_SUBJECTS',
75
- sourceMode: 'WORKFLOW',
76
- path: event.target.value,
77
- })
229
+ replace(index, event.target.value, headerValue)
230
+ }
231
+ />
232
+ <Input
233
+ aria-label={t('returnResult.headerValue')}
234
+ value={headerValue}
235
+ disabled={disabled}
236
+ placeholder="created"
237
+ onChange={(event) => replace(index, key, event.target.value)}
238
+ />
239
+ <Button
240
+ type="text"
241
+ danger
242
+ icon={<DeleteOutlined />}
243
+ aria-label={t('returnResult.deleteHeader', { name: key })}
244
+ disabled={disabled}
245
+ onClick={() =>
246
+ onChange(
247
+ Object.fromEntries(
248
+ entries.filter((_, current) => current !== index),
249
+ ),
250
+ )
78
251
  }
79
- style={{ marginTop: 6 }}
80
252
  />
81
- <Typography.Text
82
- type="secondary"
83
- style={{ display: 'block', marginTop: 6, fontSize: 12 }}
84
- >
85
- {t('returnResult.workflowHint')}
86
- </Typography.Text>
87
253
  </div>
88
- )}
254
+ ))}
255
+ <Button
256
+ type="dashed"
257
+ block
258
+ icon={<PlusOutlined />}
259
+ disabled={disabled}
260
+ onClick={() =>
261
+ onChange({
262
+ ...value,
263
+ [nextHeaderName(value)]: '',
264
+ })
265
+ }
266
+ >
267
+ {t('returnResult.addHeader')}
268
+ </Button>
269
+ <Typography.Text type="secondary">
270
+ {t('returnResult.httpHint')}
271
+ </Typography.Text>
89
272
  </Space>
90
273
  );
91
274
  }
275
+
276
+ function nextHeaderName(value: Record<string, string>): string {
277
+ if (!('X-Result-Type' in value)) return 'X-Result-Type';
278
+ let index = 2;
279
+ while (`X-Result-Type-${index}` in value) index += 1;
280
+ return `X-Result-Type-${index}`;
281
+ }
@@ -0,0 +1,152 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { customRequest } from 'helpers/http';
4
+ import { MemoryRouter } from 'react-router-dom';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import TemporalTenantPage from '.';
7
+
8
+ vi.mock('helpers/http', () => ({ customRequest: vi.fn() }));
9
+
10
+ vi.mock('@refinedev/core', () => ({
11
+ useGetIdentity: () => ({
12
+ data: {
13
+ configPerms: {
14
+ roles: ['SYSTEM_ADMIN'],
15
+ },
16
+ },
17
+ }),
18
+ }));
19
+
20
+ Object.defineProperty(window, 'matchMedia', {
21
+ writable: true,
22
+ value: () => ({
23
+ matches: false,
24
+ addListener: () => undefined,
25
+ removeListener: () => undefined,
26
+ addEventListener: () => undefined,
27
+ removeEventListener: () => undefined,
28
+ dispatchEvent: () => false,
29
+ }),
30
+ });
31
+
32
+ describe('TemporalTenantPage', () => {
33
+ beforeEach(() => {
34
+ const getComputedStyle = window.getComputedStyle.bind(window);
35
+ vi.spyOn(window, 'getComputedStyle').mockImplementation((element) =>
36
+ getComputedStyle(element),
37
+ );
38
+
39
+ let config = {
40
+ id: 1,
41
+ adminUrl: 'http://temporal-worker:8081',
42
+ adminToken: 'admin-token-1234567890',
43
+ namespace: 'gadmin-test',
44
+ dbUrl: 'postgresql://gadmin:db-password@postgres:5432/gadmin',
45
+ displayName: 'GAdmin Workflow',
46
+ runtimeBaseUrl: 'http://gadmin-server:8000/api',
47
+ runtimeToken: 'runtime-token-1234567890-runtime',
48
+ createdAt: '2026-07-01T00:00:00.000Z',
49
+ updatedAt: '2026-07-30T00:00:00.000Z',
50
+ };
51
+
52
+ vi.mocked(customRequest).mockImplementation(
53
+ async (resource: string, method: string, values?: any) => {
54
+ if (resource === 'temporalTenantConfig/findMany' && method === 'POST') {
55
+ return {
56
+ data: [config],
57
+ meta: {
58
+ itemCount: 1,
59
+ pageCount: 1,
60
+ hasPreviousPage: false,
61
+ hasNextPage: false,
62
+ },
63
+ } as never;
64
+ }
65
+
66
+ if (
67
+ resource === 'temporalTenantConfig/updateUnique/1' &&
68
+ method === 'PATCH'
69
+ ) {
70
+ config = { ...config, ...values };
71
+ return config as never;
72
+ }
73
+
74
+ return {} as never;
75
+ },
76
+ );
77
+ });
78
+
79
+ afterEach(() => vi.restoreAllMocks());
80
+
81
+ it('shows the registered configuration and saves changes from the edit drawer', async () => {
82
+ const user = userEvent.setup();
83
+
84
+ render(
85
+ <MemoryRouter>
86
+ <TemporalTenantPage />
87
+ </MemoryRouter>,
88
+ );
89
+
90
+ expect(
91
+ (await screen.findAllByText('GAdmin Workflow')).length,
92
+ ).toBeGreaterThan(0);
93
+ expect(screen.getAllByText('gadmin-test').length).toBeGreaterThan(0);
94
+ expect(screen.getByText('http://temporal-worker:8081')).toBeInTheDocument();
95
+ expect(
96
+ screen.queryByDisplayValue('GAdmin Workflow'),
97
+ ).not.toBeInTheDocument();
98
+ expect(
99
+ screen.queryByText('admin-token-1234567890'),
100
+ ).not.toBeInTheDocument();
101
+
102
+ await user.click(screen.getByRole('button', { name: '显示 Admin Token' }));
103
+ expect(screen.getByText('admin-token-1234567890')).toBeInTheDocument();
104
+
105
+ await user.click(screen.getByRole('button', { name: '修改配置' }));
106
+
107
+ const namespaceInput = screen.getByLabelText('Namespace');
108
+ expect(namespaceInput).toBeDisabled();
109
+ expect(screen.getByLabelText('Admin URL')).toHaveValue(
110
+ 'http://temporal-worker:8081',
111
+ );
112
+ expect(screen.getByLabelText('Runtime API 地址')).toHaveValue(
113
+ 'http://gadmin-server:8000/api',
114
+ );
115
+
116
+ const displayNameInput = screen.getByLabelText('显示名称');
117
+ await user.clear(displayNameInput);
118
+ await user.type(displayNameInput, 'GAdmin Workflow Updated');
119
+ await user.click(screen.getByRole('button', { name: '保存修改' }));
120
+
121
+ await waitFor(() =>
122
+ expect(customRequest).toHaveBeenCalledWith(
123
+ 'temporalTenantConfig/updateUnique/1',
124
+ 'PATCH',
125
+ expect.objectContaining({
126
+ namespace: 'gadmin-test',
127
+ displayName: 'GAdmin Workflow Updated',
128
+ }),
129
+ ),
130
+ );
131
+ expect(
132
+ (await screen.findAllByText('GAdmin Workflow Updated')).length,
133
+ ).toBeGreaterThan(0);
134
+ });
135
+
136
+ it('does not present registration as available when the current status cannot be loaded', async () => {
137
+ vi.mocked(customRequest).mockRejectedValueOnce(
138
+ new Error('request failed') as never,
139
+ );
140
+
141
+ render(
142
+ <MemoryRouter>
143
+ <TemporalTenantPage />
144
+ </MemoryRouter>,
145
+ );
146
+
147
+ expect(await screen.findByText('无法确认注册状态')).toBeInTheDocument();
148
+ expect(
149
+ screen.queryByRole('button', { name: '注册服务' }),
150
+ ).not.toBeInTheDocument();
151
+ });
152
+ });