@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
@@ -17,6 +17,12 @@ import {
17
17
  message,
18
18
  } from 'antd';
19
19
  import { customRequest } from 'helpers/http';
20
+ import { PageHeader } from 'components';
21
+ import {
22
+ NESTED_CARD_PROPS,
23
+ PAGE_WRAPPER_STYLE,
24
+ SEARCH_CARD_PROPS,
25
+ } from 'constants/layout';
20
26
  import { useCallback, useEffect, useState } from 'react';
21
27
  import { useTranslation } from 'react-i18next';
22
28
  import { ApprovalChannelModal } from './ApprovalChannelModal';
@@ -48,50 +54,41 @@ export default function ApprovalChannelsPage() {
48
54
  useEffect(() => void load(), [load]);
49
55
 
50
56
  return (
51
- <div style={{ padding: 24 }}>
52
- <Card>
53
- <div
54
- style={{
55
- display: 'flex',
56
- justifyContent: 'space-between',
57
- alignItems: 'flex-start',
58
- gap: 16,
59
- marginBottom: 20,
60
- }}
61
- >
62
- <div>
63
- <Typography.Title level={3} style={{ margin: 0 }}>
64
- {t('approvalChannel.title')}
65
- </Typography.Title>
66
- <Typography.Text type="secondary">
67
- {t('approvalChannel.subtitle')}
68
- </Typography.Text>
69
- </div>
70
- <Button
71
- type="primary"
72
- icon={<PlusOutlined />}
73
- onClick={() => setEditing(null)}
74
- >
75
- {t('approvalChannel.newChannel')}
76
- </Button>
77
- </div>
57
+ <div style={PAGE_WRAPPER_STYLE}>
58
+ <PageHeader
59
+ title={t('approvalChannel.title')}
60
+ subtitle={t('approvalChannel.subtitle')}
61
+ extra={
62
+ <Space>
63
+ <Button icon={<ReloadOutlined />} onClick={load} />
64
+ <Button
65
+ type="primary"
66
+ icon={<PlusOutlined />}
67
+ onClick={() => setEditing(null)}
68
+ >
69
+ {t('approvalChannel.newChannel')}
70
+ </Button>
71
+ </Space>
72
+ }
73
+ />
78
74
 
79
- <Space style={{ marginBottom: 18 }}>
80
- <Input.Search
81
- allowClear
82
- placeholder={t('approvalChannel.search')}
83
- onSearch={setQuery}
84
- style={{ width: 300 }}
85
- />
86
- <Button icon={<ReloadOutlined />} onClick={load}>
87
- {t('common.refresh')}
88
- </Button>
89
- </Space>
75
+ {/* Search Card */}
76
+ <Card {...SEARCH_CARD_PROPS}>
77
+ <Input.Search
78
+ allowClear
79
+ placeholder={t('approvalChannel.search')}
80
+ onSearch={setQuery}
81
+ style={{ width: 300 }}
82
+ />
83
+ </Card>
90
84
 
85
+ {/* Content Card */}
86
+ <Card>
91
87
  {loading ? (
92
- <div style={{ textAlign: 'center', padding: 60 }}>
93
- <Spin />
94
- </div>
88
+ <Spin
89
+ size="large"
90
+ style={{ display: 'block', margin: '48px auto' }}
91
+ />
95
92
  ) : channels.length === 0 ? (
96
93
  <Empty description={t('approvalChannel.empty')} />
97
94
  ) : (
@@ -103,7 +100,12 @@ export default function ApprovalChannelsPage() {
103
100
  }}
104
101
  >
105
102
  {channels.map((channel) => (
106
- <Card key={channel.code} size="small" hoverable>
103
+ <Card
104
+ key={channel.code}
105
+ size="small"
106
+ hoverable
107
+ {...NESTED_CARD_PROPS}
108
+ >
107
109
  <div
108
110
  style={{
109
111
  display: 'flex',
@@ -189,9 +191,9 @@ function WorkflowStep({
189
191
  style={{
190
192
  minWidth: 0,
191
193
  padding: '10px 12px',
192
- border: '1px solid #d9d9d9',
194
+ border: '1px solid var(--ant-color-border-secondary)',
193
195
  borderRadius: 8,
194
- background: '#fafafa',
196
+ background: 'var(--ant-color-fill-quaternary)',
195
197
  }}
196
198
  >
197
199
  <Space size={6}>
@@ -117,7 +117,7 @@ describe.each([
117
117
  ['FlowRenderer', FlowRenderer],
118
118
  ['EnhancedFlowRenderer', EnhancedFlowRenderer],
119
119
  ] as const)('%s new-node defaults', (_name, Renderer) => {
120
- it('gives a new Return Result node an approval result from the previous step', () => {
120
+ it('gives a new Return Result node ordinary data from the previous step', () => {
121
121
  const onDslChange = vi.fn();
122
122
  render(
123
123
  <Renderer dsl={{ nodes: [], edges: [] }} onDslChange={onDslChange} />,
@@ -131,8 +131,10 @@ describe.each([
131
131
  expect.objectContaining({
132
132
  type: 'return_result',
133
133
  config: expect.objectContaining({
134
- resultType: 'APPROVAL_SUBJECTS',
134
+ resultType: 'JSON',
135
135
  sourceMode: 'INPUT',
136
+ httpStatusCode: 200,
137
+ httpHeaders: {},
136
138
  }),
137
139
  }),
138
140
  ],
@@ -187,6 +189,8 @@ describe.each([
187
189
  path: expect.stringMatching(/^external-/),
188
190
  secret: expect.stringMatching(/^[a-f0-9]{64}$/),
189
191
  verificationMode: 'HMAC',
192
+ responseMode: 'IMMEDIATE',
193
+ responseTimeoutSeconds: 60,
190
194
  }),
191
195
  }),
192
196
  ],
@@ -18,6 +18,12 @@ import {
18
18
  ReloadOutlined,
19
19
  } from '@ant-design/icons';
20
20
  import { customRequest } from 'helpers/http';
21
+ import { PageHeader } from 'components';
22
+ import {
23
+ NESTED_CARD_PROPS,
24
+ PAGE_WRAPPER_STYLE,
25
+ SEARCH_CARD_PROPS,
26
+ } from 'constants/layout';
21
27
  import { ConnectionModal } from './ConnectionModal';
22
28
  import type { IntegrationConnection } from './connection.types';
23
29
  import { useTranslation } from 'react-i18next';
@@ -50,47 +56,41 @@ export default function ConnectionsPage() {
50
56
  useEffect(() => void load(), [load]);
51
57
 
52
58
  return (
53
- <div style={{ padding: 24 }}>
59
+ <div style={PAGE_WRAPPER_STYLE}>
60
+ <PageHeader
61
+ title={t('connection.title')}
62
+ subtitle={t('connection.subtitle')}
63
+ extra={
64
+ <Space>
65
+ <Button icon={<ReloadOutlined />} onClick={load} />
66
+ <Button
67
+ type="primary"
68
+ icon={<PlusOutlined />}
69
+ onClick={() => setEditing(null)}
70
+ >
71
+ {t('connection.new')}
72
+ </Button>
73
+ </Space>
74
+ }
75
+ />
76
+
77
+ {/* Search Card */}
78
+ <Card {...SEARCH_CARD_PROPS}>
79
+ <Input.Search
80
+ allowClear
81
+ placeholder={t('connection.search')}
82
+ onSearch={setQuery}
83
+ style={{ width: 300 }}
84
+ />
85
+ </Card>
86
+
87
+ {/* Content Card */}
54
88
  <Card>
55
- <div
56
- style={{
57
- display: 'flex',
58
- justifyContent: 'space-between',
59
- gap: 16,
60
- marginBottom: 20,
61
- }}
62
- >
63
- <div>
64
- <Typography.Title level={3} style={{ margin: 0 }}>
65
- {t('connection.title')}
66
- </Typography.Title>
67
- <Typography.Text type="secondary">
68
- {t('connection.subtitle')}
69
- </Typography.Text>
70
- </div>
71
- <Button
72
- type="primary"
73
- icon={<PlusOutlined />}
74
- onClick={() => setEditing(null)}
75
- >
76
- {t('connection.new')}
77
- </Button>
78
- </div>
79
- <Space style={{ marginBottom: 18 }}>
80
- <Input.Search
81
- allowClear
82
- placeholder={t('connection.search')}
83
- onSearch={setQuery}
84
- style={{ width: 300 }}
85
- />
86
- <Button icon={<ReloadOutlined />} onClick={load}>
87
- {t('common.refresh')}
88
- </Button>
89
- </Space>
90
89
  {loading ? (
91
- <div style={{ textAlign: 'center', padding: 60 }}>
92
- <Spin />
93
- </div>
90
+ <Spin
91
+ size="large"
92
+ style={{ display: 'block', margin: '48px auto' }}
93
+ />
94
94
  ) : connections.length === 0 ? (
95
95
  <Empty description={t('connection.empty')} />
96
96
  ) : (
@@ -102,7 +102,12 @@ export default function ConnectionsPage() {
102
102
  }}
103
103
  >
104
104
  {connections.map((connection) => (
105
- <Card key={connection.code} size="small" hoverable>
105
+ <Card
106
+ key={connection.code}
107
+ size="small"
108
+ hoverable
109
+ {...NESTED_CARD_PROPS}
110
+ >
106
111
  <div
107
112
  style={{
108
113
  display: 'flex',
@@ -112,7 +117,11 @@ export default function ConnectionsPage() {
112
117
  >
113
118
  <Space align="start">
114
119
  <ApiOutlined
115
- style={{ color: '#1677ff', fontSize: 20, marginTop: 3 }}
120
+ style={{
121
+ color: 'var(--ant-color-primary)',
122
+ fontSize: 20,
123
+ marginTop: 3,
124
+ }}
116
125
  />
117
126
  <div>
118
127
  <Typography.Text strong>
@@ -11,7 +11,6 @@ import {
11
11
  Table,
12
12
  Tag,
13
13
  Tooltip,
14
- Typography,
15
14
  message,
16
15
  } from 'antd';
17
16
  import {
@@ -33,8 +32,9 @@ import { ExportModal } from './components/ExportModal';
33
32
  import { ImportModal } from './components/ImportModal';
34
33
  import type { Workflow, WorkflowOverview } from './types';
35
34
  import { useTranslation } from 'react-i18next';
35
+ import { PageHeader } from 'components';
36
+ import { PAGE_WRAPPER_STYLE, SEARCH_CARD_PROPS } from 'constants/layout';
36
37
 
37
- const { Title } = Typography;
38
38
  const PAGE_SIZE = 20;
39
39
 
40
40
  function RunDot({
@@ -402,23 +402,10 @@ export default function WorkflowListPage() {
402
402
  ];
403
403
 
404
404
  return (
405
- <div
406
- style={{
407
- background: 'var(--ant-color-bg-layout)',
408
- minHeight: 'calc(100vh - 64px)',
409
- }}
410
- >
411
- <Card style={{ marginBottom: 16 }}>
412
- <div
413
- style={{
414
- display: 'flex',
415
- justifyContent: 'space-between',
416
- alignItems: 'center',
417
- }}
418
- >
419
- <Title level={4} style={{ margin: 0 }}>
420
- {t('list.title')}
421
- </Title>
405
+ <div style={PAGE_WRAPPER_STYLE}>
406
+ <PageHeader
407
+ title={t('list.title')}
408
+ extra={
422
409
  <Space>
423
410
  <Tooltip title="查看 WorkflowEventOutbox 表原始数据">
424
411
  <Button
@@ -445,8 +432,8 @@ export default function WorkflowListPage() {
445
432
  </Button>
446
433
  <Button icon={<ReloadOutlined />} onClick={fetchData} />
447
434
  </Space>
448
- </div>
449
- </Card>
435
+ }
436
+ />
450
437
 
451
438
  <StatusCards
452
439
  overview={overview}
@@ -454,7 +441,7 @@ export default function WorkflowListPage() {
454
441
  onFilter={setFilterInstanceStatus}
455
442
  />
456
443
 
457
- <Card style={{ marginBottom: 16 }}>
444
+ <Card {...SEARCH_CARD_PROPS}>
458
445
  <Space wrap>
459
446
  <Select
460
447
  placeholder="Status"
@@ -12,6 +12,7 @@ import {
12
12
  import { ArrowLeftOutlined } from '@ant-design/icons';
13
13
  import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
14
14
  import { customRequest } from 'helpers/http';
15
+ import { HEADER_CARD_PROPS, PAGE_WRAPPER_STYLE } from 'constants/layout';
15
16
 
16
17
  const { Title } = Typography;
17
18
  const PAGE_SIZE = 20;
@@ -183,13 +184,8 @@ export default function WorkflowInstancesPage() {
183
184
  ];
184
185
 
185
186
  return (
186
- <div
187
- style={{
188
- background: 'var(--ant-color-bg-layout)',
189
- minHeight: 'calc(100vh - 64px)',
190
- }}
191
- >
192
- <Card style={{ marginBottom: 16 }}>
187
+ <div style={PAGE_WRAPPER_STYLE}>
188
+ <Card {...HEADER_CARD_PROPS}>
193
189
  <div
194
190
  style={{
195
191
  display: 'flex',
@@ -224,7 +220,7 @@ export default function WorkflowInstancesPage() {
224
220
  </div>
225
221
  </Card>
226
222
 
227
- <Card>
223
+ <Card className="card-table">
228
224
  <Table
229
225
  rowKey="id"
230
226
  loading={loading}
@@ -10,7 +10,6 @@ import {
10
10
  Table,
11
11
  Tag,
12
12
  Tooltip,
13
- Typography,
14
13
  message,
15
14
  } from 'antd';
16
15
  import {
@@ -22,9 +21,10 @@ import {
22
21
  } from '@ant-design/icons';
23
22
  import { useSearchParams, useNavigate } from 'react-router-dom';
24
23
  import { customRequest } from 'helpers/http';
24
+ import { PageHeader } from 'components';
25
+ import { PAGE_WRAPPER_STYLE, SEARCH_CARD_PROPS } from 'constants/layout';
25
26
  import type { WorkflowNodeType } from '../types';
26
27
 
27
- const { Title } = Typography;
28
28
  const PAGE_SIZE = 20;
29
29
 
30
30
  interface NodeInstance {
@@ -226,23 +226,10 @@ export default function NodeInstanceListPage() {
226
226
  ];
227
227
 
228
228
  return (
229
- <div
230
- style={{
231
- background: 'var(--ant-color-bg-layout)',
232
- minHeight: 'calc(100vh - 64px)',
233
- }}
234
- >
235
- <Card style={{ marginBottom: 16 }}>
236
- <div
237
- style={{
238
- display: 'flex',
239
- justifyContent: 'space-between',
240
- alignItems: 'center',
241
- }}
242
- >
243
- <Title level={4} style={{ margin: 0 }}>
244
- Node instance mgnts
245
- </Title>
229
+ <div style={PAGE_WRAPPER_STYLE}>
230
+ <PageHeader
231
+ title="Node instance mgnts"
232
+ extra={
246
233
  <Space>
247
234
  <Button
248
235
  type="primary"
@@ -253,10 +240,10 @@ export default function NodeInstanceListPage() {
253
240
  </Button>
254
241
  <Button icon={<ReloadOutlined />} onClick={fetchData} />
255
242
  </Space>
256
- </div>
257
- </Card>
243
+ }
244
+ />
258
245
 
259
- <Card style={{ marginBottom: 16 }}>
246
+ <Card {...SEARCH_CARD_PROPS}>
260
247
  <Space wrap>
261
248
  <Select
262
249
  placeholder="Node Type"
@@ -90,4 +90,46 @@ describe('ExternalRequestConfigEditor', () => {
90
90
  screen.getByText(/Anyone who knows this endpoint/),
91
91
  ).toBeInTheDocument();
92
92
  });
93
+
94
+ it('lets the caller wait for the final Workflow return value', async () => {
95
+ const onChange = vi.fn();
96
+
97
+ function Harness() {
98
+ const [value, setValue] = useState<
99
+ NonNullable<Parameters<typeof ExternalRequestConfigEditor>[0]['value']>
100
+ >({
101
+ path: 'create-ticket',
102
+ method: 'POST',
103
+ verificationMode: 'NONE',
104
+ responseMode: 'IMMEDIATE',
105
+ });
106
+ return (
107
+ <ExternalRequestConfigEditor
108
+ value={value}
109
+ onChange={(next) => {
110
+ onChange(next);
111
+ setValue(next);
112
+ }}
113
+ schema={schema}
114
+ />
115
+ );
116
+ }
117
+
118
+ render(<Harness />);
119
+ await screen.findByDisplayValue(
120
+ 'http://10.20.30.40:14003/api/workflow/webhook/myoa-callback',
121
+ );
122
+ fireEvent.click(
123
+ screen.getByRole('radio', { name: /Wait for Workflow result/ }),
124
+ );
125
+
126
+ expect(onChange).toHaveBeenLastCalledWith({
127
+ path: 'create-ticket',
128
+ method: 'POST',
129
+ verificationMode: 'NONE',
130
+ responseMode: 'WAIT_FOR_RESULT',
131
+ responseTimeoutSeconds: 60,
132
+ });
133
+ expect(screen.getByRole('spinbutton')).toHaveValue('60');
134
+ });
93
135
  });
@@ -4,6 +4,7 @@ import {
4
4
  Button,
5
5
  Card,
6
6
  Input,
7
+ InputNumber,
7
8
  Radio,
8
9
  Space,
9
10
  Tag,
@@ -20,6 +21,8 @@ type ExternalRequestConfig = {
20
21
  method?: string;
21
22
  secret?: string;
22
23
  verificationMode?: 'HMAC' | 'NONE';
24
+ responseMode?: 'IMMEDIATE' | 'WAIT_FOR_RESULT';
25
+ responseTimeoutSeconds?: number;
23
26
  };
24
27
 
25
28
  export function ExternalRequestConfigEditor({
@@ -30,6 +33,8 @@ export function ExternalRequestConfigEditor({
30
33
  const { t } = useTranslation('workflow');
31
34
  const config = value ?? {};
32
35
  const verificationMode = config.verificationMode === 'NONE' ? 'NONE' : 'HMAC';
36
+ const responseMode =
37
+ config.responseMode === 'WAIT_FOR_RESULT' ? 'WAIT_FOR_RESULT' : 'IMMEDIATE';
33
38
  const [endpoint, setEndpoint] = useState('');
34
39
  const [endpointLoadFailed, setEndpointLoadFailed] = useState(false);
35
40
 
@@ -101,6 +106,78 @@ export function ExternalRequestConfigEditor({
101
106
  </Space>
102
107
  </Card>
103
108
 
109
+ <Card size="small" title={t('externalRequest.response')}>
110
+ <Radio.Group
111
+ value={responseMode}
112
+ disabled={disabled}
113
+ onChange={(event) => {
114
+ const nextMode = event.target.value as
115
+ | 'IMMEDIATE'
116
+ | 'WAIT_FOR_RESULT';
117
+ onChange({
118
+ ...config,
119
+ method: 'POST',
120
+ responseMode: nextMode,
121
+ ...(nextMode === 'WAIT_FOR_RESULT' && {
122
+ responseTimeoutSeconds:
123
+ config.responseTimeoutSeconds &&
124
+ config.responseTimeoutSeconds >= 1 &&
125
+ config.responseTimeoutSeconds <= 300
126
+ ? config.responseTimeoutSeconds
127
+ : 60,
128
+ }),
129
+ });
130
+ }}
131
+ >
132
+ <Space direction="vertical" size={12}>
133
+ <Radio value="IMMEDIATE">
134
+ <Space direction="vertical" size={0}>
135
+ <Typography.Text>
136
+ {t('externalRequest.respondImmediately')}
137
+ </Typography.Text>
138
+ <Typography.Text type="secondary">
139
+ {t('externalRequest.respondImmediatelyHint')}
140
+ </Typography.Text>
141
+ </Space>
142
+ </Radio>
143
+ <Radio value="WAIT_FOR_RESULT">
144
+ <Space direction="vertical" size={0}>
145
+ <Typography.Text>
146
+ {t('externalRequest.waitForResult')}
147
+ </Typography.Text>
148
+ <Typography.Text type="secondary">
149
+ {t('externalRequest.waitForResultHint')}
150
+ </Typography.Text>
151
+ </Space>
152
+ </Radio>
153
+ </Space>
154
+ </Radio.Group>
155
+ {responseMode === 'WAIT_FOR_RESULT' && (
156
+ <div style={{ marginTop: 16 }}>
157
+ <Typography.Text>
158
+ {t('externalRequest.responseTimeout')}
159
+ </Typography.Text>
160
+ <InputNumber
161
+ min={1}
162
+ max={300}
163
+ precision={0}
164
+ value={config.responseTimeoutSeconds ?? 60}
165
+ disabled={disabled}
166
+ addonAfter={t('externalRequest.seconds')}
167
+ onChange={(next) =>
168
+ onChange({
169
+ ...config,
170
+ method: 'POST',
171
+ responseMode: 'WAIT_FOR_RESULT',
172
+ responseTimeoutSeconds: next ?? 60,
173
+ })
174
+ }
175
+ style={{ display: 'block', marginTop: 6, width: '100%' }}
176
+ />
177
+ </div>
178
+ )}
179
+ </Card>
180
+
104
181
  <Card size="small" title={t('externalRequest.verification')}>
105
182
  <Radio.Group
106
183
  value={verificationMode}
@@ -11,10 +11,11 @@ describe('ReturnResultConfigEditor', () => {
11
11
  });
12
12
 
13
13
  it('uses the connected previous node by default without asking for a path', () => {
14
+ const onChange = vi.fn();
14
15
  render(
15
16
  <ReturnResultConfigEditor
16
17
  value={undefined}
17
- onChange={vi.fn()}
18
+ onChange={onChange}
18
19
  schema={schema}
19
20
  />,
20
21
  );
@@ -23,6 +24,12 @@ describe('ReturnResultConfigEditor', () => {
23
24
  screen.getByText('Return the previous node result'),
24
25
  ).toBeInTheDocument();
25
26
  expect(screen.queryByLabelText('Workflow data field')).toBeNull();
27
+ expect(onChange).toHaveBeenCalledWith({
28
+ resultType: 'JSON',
29
+ sourceMode: 'INPUT',
30
+ httpStatusCode: 200,
31
+ httpHeaders: {},
32
+ });
26
33
  });
27
34
 
28
35
  it('can return a field from the original Workflow data', () => {
@@ -50,4 +57,25 @@ describe('ReturnResultConfigEditor', () => {
50
57
  sourceMode: 'INPUT',
51
58
  });
52
59
  });
60
+
61
+ it('configures ordinary data with an HTTP status for external callers', () => {
62
+ const onChange = vi.fn();
63
+ render(
64
+ <ReturnResultConfigEditor
65
+ value={{
66
+ resultType: 'JSON',
67
+ sourceMode: 'INPUT',
68
+ httpStatusCode: 201,
69
+ httpHeaders: {},
70
+ }}
71
+ onChange={onChange}
72
+ schema={schema}
73
+ />,
74
+ );
75
+
76
+ expect(screen.getByRole('radio', { name: 'Ordinary data' })).toBeChecked();
77
+ expect(
78
+ screen.getByRole('spinbutton', { name: 'HTTP status code' }),
79
+ ).toHaveValue('201');
80
+ });
53
81
  });