@gadmin2n/schematics 0.0.149 → 0.0.151

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 (81) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +125 -36
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +42 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +2 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +0 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +3 -1
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-dev-env.js +218 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +78 -12
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +37 -67
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -17
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +46 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +60 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +3 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +9 -1
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +55 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +491 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.dto.ts +213 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.module.ts +28 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/resource-transfer.service.ts +137 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/role-transfer.service.ts +248 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-token.service.ts +65 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/user-transfer.service.ts +146 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +55 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +2 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +55 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +2 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.spec.ts +46 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.ts +3 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +55 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +2 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +1 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +96 -14
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +41 -41
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.spec.ts +36 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.ts +90 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +4 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +1 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +115 -63
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.spec.ts +166 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.ts +217 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +33 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +57 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +196 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.ts +125 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +9 -3
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +148 -4
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +12 -1
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +14 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +75 -45
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +25 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +115 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +257 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +40 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +15 -13
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +40 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +15 -13
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -1
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +89 -65
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +51 -127
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +53 -109
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +45 -41
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +51 -72
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +49 -40
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +16 -5
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +28 -10
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +60 -1
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +94 -12
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +13 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +3 -3
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +8 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +167 -94
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +95 -95
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.test.tsx +81 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.tsx +126 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +48 -52
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +2 -2
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -3
  79. package/package.json +1 -1
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/ac.config.ts +0 -24
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +0 -495
@@ -1,51 +1,46 @@
1
- import { InputNumber, Space, Switch, Tag, Typography } from 'antd';
1
+ import { Alert, Button, InputNumber, Select, Space, Typography } from 'antd';
2
+ import { customRequest } from 'helpers/http';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
4
  import { useTranslation } from 'react-i18next';
3
- import { WorkflowCodeEditor } from '../../components/WorkflowCodeEditor';
5
+ import { ParameterBindingEditor } from '../../components/ParameterBindingEditor';
4
6
  import type { ParameterBinding } from '../../types/parameter-binding';
5
7
  import type { WidgetProps } from '../schema-form/widget-registry';
6
- import { ParameterBindingListEditor } from './ParameterBindingListEditor';
8
+
9
+ export interface DatabaseFunctionArgument {
10
+ position: number;
11
+ name: string;
12
+ type: string;
13
+ }
14
+
15
+ export interface DatabaseFunctionOption {
16
+ schema: string;
17
+ name: string;
18
+ identityArguments: string;
19
+ returnType: string;
20
+ arguments: DatabaseFunctionArgument[];
21
+ }
22
+
23
+ type DatabaseFunctionRef = Pick<
24
+ DatabaseFunctionOption,
25
+ 'schema' | 'name' | 'identityArguments'
26
+ >;
7
27
 
8
28
  type DbExecuteConfig = {
9
- allowWrite?: boolean;
10
- sql?: string;
11
- parameters?: ParameterBinding[];
29
+ functionRef?: DatabaseFunctionRef;
30
+ argumentBindings?: ParameterBinding[];
12
31
  timeoutSeconds?: number | null;
13
32
  };
14
33
 
15
- function normalizeSql(sql: string): string {
16
- const withoutContent = sql
17
- .replace(/\$(\w*)\$[\s\S]*?\$\1\$/g, ' ')
18
- .replace(/'(?:''|[^'])*'/g, ' ')
19
- .replace(/"(?:""|[^"])*"/g, ' ')
20
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
21
- .replace(/--[^\r\n]*/g, ' ')
22
- .trim();
23
- const withoutTrailingSemicolon = withoutContent.replace(/;\s*$/, '').trim();
24
- return withoutTrailingSemicolon.includes(';') ? '' : withoutTrailingSemicolon;
34
+ function functionKey(value: DatabaseFunctionRef): string {
35
+ return `${value.schema}.${value.name}(${value.identityArguments})`;
25
36
  }
26
37
 
27
- export function isDatabaseSqlAllowed(
28
- sql: string,
29
- allowWrite: boolean,
30
- ): boolean {
31
- const normalized = normalizeSql(sql);
32
- if (!normalized) return false;
33
- const readOnly =
34
- /^(select|with)\b/i.test(normalized) &&
35
- !/\b(insert|update|delete|merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
36
- normalized,
37
- );
38
- const writableCte =
39
- /^with\b/i.test(normalized) &&
40
- /\b(insert|update|delete)\b/i.test(normalized) &&
41
- !/\b(merge|call|copy|create|alter|drop|truncate|grant|revoke|vacuum|refresh)\b/i.test(
42
- normalized,
43
- );
44
- return (
45
- readOnly ||
46
- (allowWrite &&
47
- (/^(insert|update|delete)\b/i.test(normalized) || writableCte))
48
- );
38
+ function defaultArgumentPath(name: string): string {
39
+ return name.replace(/^p_/, '') || name;
40
+ }
41
+
42
+ function defaultBinding(argument: DatabaseFunctionArgument): ParameterBinding {
43
+ return { source: 'INPUT', path: defaultArgumentPath(argument.name) };
49
44
  }
50
45
 
51
46
  export function DbExecuteConfigEditor({
@@ -55,71 +50,149 @@ export function DbExecuteConfigEditor({
55
50
  }: WidgetProps<DbExecuteConfig | undefined>) {
56
51
  const { t } = useTranslation('workflow');
57
52
  const config = value ?? {};
58
- const allowWrite = config.allowWrite === true;
53
+ const [functions, setFunctions] = useState<DatabaseFunctionOption[]>([]);
54
+ const [catalogStatus, setCatalogStatus] = useState<
55
+ 'loading' | 'ready' | 'error'
56
+ >('loading');
57
+
58
+ const loadFunctions = useCallback(async () => {
59
+ setCatalogStatus('loading');
60
+ try {
61
+ const items = await customRequest<DatabaseFunctionOption[]>(
62
+ 'workflow/database-functions',
63
+ 'GET',
64
+ );
65
+ setFunctions(Array.isArray(items) ? items : []);
66
+ setCatalogStatus('ready');
67
+ } catch {
68
+ setCatalogStatus('error');
69
+ }
70
+ }, []);
71
+
72
+ useEffect(() => {
73
+ void loadFunctions();
74
+ }, [loadFunctions]);
75
+
76
+ const selected = useMemo(
77
+ () =>
78
+ config.functionRef
79
+ ? functions.find(
80
+ (item) => functionKey(item) === functionKey(config.functionRef!),
81
+ )
82
+ : undefined,
83
+ [config.functionRef, functions],
84
+ );
85
+ const selectedKey = config.functionRef
86
+ ? functionKey(config.functionRef)
87
+ : undefined;
59
88
 
60
89
  return (
61
90
  <Space direction="vertical" size={14} style={{ width: '100%' }}>
62
- <div className="db-execute-config__permission">
63
- <div>
64
- <Typography.Text strong>
65
- {t('nodeEditors.database.allowWrite')}
66
- </Typography.Text>
67
- <Typography.Text type="secondary" className="db-execute-config__hint">
68
- {t(
69
- allowWrite
70
- ? 'nodeEditors.database.readWriteDescription'
71
- : 'nodeEditors.database.readOnlyDescription',
72
- )}
73
- </Typography.Text>
74
- </div>
75
- <Switch
76
- aria-label={t('nodeEditors.database.allowWrite')}
77
- checked={allowWrite}
78
- disabled={disabled}
79
- onChange={(nextAllowWrite) =>
80
- onChange({ ...config, allowWrite: nextAllowWrite })
81
- }
82
- />
83
- </div>
84
-
85
91
  <Space direction="vertical" size={6} style={{ width: '100%' }}>
86
- <div className="db-execute-config__sql-head">
87
- <Typography.Text strong>
88
- {t('nodeEditors.database.sql')}
92
+ <Typography.Text>{t('nodeEditors.database.function')}</Typography.Text>
93
+ <Typography.Text type="secondary">
94
+ {t('nodeEditors.database.functionHint')}
95
+ </Typography.Text>
96
+ {catalogStatus === 'error' && (
97
+ <Typography.Text type="danger">
98
+ {t('nodeEditors.database.loadFailed')}{' '}
99
+ <Button type="link" size="small" onClick={loadFunctions}>
100
+ {t('common.retry')}
101
+ </Button>
89
102
  </Typography.Text>
90
- <Tag bordered={false} color={allowWrite ? 'orange' : 'green'}>
91
- {t(
92
- allowWrite
93
- ? 'nodeEditors.database.writeEnabled'
94
- : 'nodeEditors.database.readOnly',
95
- )}
96
- </Tag>
97
- </div>
98
- <WorkflowCodeEditor
99
- language="sql"
100
- value={config.sql ?? ''}
101
- readOnly={disabled}
102
- height={280}
103
- wordWrap="off"
104
- ariaLabel={t('nodeEditors.database.sql')}
105
- onChange={(sql) => onChange({ ...config, sql })}
103
+ )}
104
+ <Select
105
+ aria-label={t('nodeEditors.database.function')}
106
+ value={selectedKey}
107
+ disabled={disabled || catalogStatus !== 'ready'}
108
+ loading={catalogStatus === 'loading'}
109
+ placeholder={t('nodeEditors.database.selectFunction')}
110
+ showSearch
111
+ optionFilterProp="label"
112
+ options={functions.map((item) => ({
113
+ value: functionKey(item),
114
+ label: functionKey(item),
115
+ }))}
116
+ onChange={(key) => {
117
+ const databaseFunction = functions.find(
118
+ (item) => functionKey(item) === key,
119
+ );
120
+ if (!databaseFunction) return;
121
+ const keepExistingBindings = selectedKey === key;
122
+ onChange({
123
+ functionRef: {
124
+ schema: databaseFunction.schema,
125
+ name: databaseFunction.name,
126
+ identityArguments: databaseFunction.identityArguments,
127
+ },
128
+ argumentBindings: databaseFunction.arguments.map(
129
+ (argument, index) =>
130
+ (keepExistingBindings
131
+ ? config.argumentBindings?.[index]
132
+ : undefined) ?? defaultBinding(argument),
133
+ ),
134
+ timeoutSeconds: config.timeoutSeconds ?? 30,
135
+ });
136
+ }}
137
+ style={{ width: '100%' }}
106
138
  />
107
139
  </Space>
108
140
 
109
- <Space direction="vertical" size={6} style={{ width: '100%' }}>
110
- <Typography.Text>
111
- {t('nodeEditors.database.parameters')}
112
- </Typography.Text>
113
- <Typography.Text type="secondary" className="db-execute-config__hint">
114
- {t('nodeEditors.database.parametersHint')}
115
- </Typography.Text>
116
- <ParameterBindingListEditor
117
- value={config.parameters}
118
- disabled={disabled}
119
- addLabel={t('nodeEditors.database.addParameter')}
120
- onChange={(parameters) => onChange({ ...config, parameters })}
141
+ {catalogStatus === 'ready' && config.functionRef && !selected && (
142
+ <Alert
143
+ type="warning"
144
+ showIcon
145
+ message={t('nodeEditors.database.functionMissing')}
121
146
  />
122
- </Space>
147
+ )}
148
+
149
+ {selected && (
150
+ <>
151
+ <Typography.Text type="secondary">
152
+ {t('nodeEditors.database.returnType', {
153
+ type: selected.returnType,
154
+ })}
155
+ </Typography.Text>
156
+ {selected.arguments.length === 0 ? (
157
+ <Typography.Text type="secondary">
158
+ {t('nodeEditors.database.noArguments')}
159
+ </Typography.Text>
160
+ ) : (
161
+ selected.arguments.map((argument, index) => (
162
+ <Space
163
+ key={`${argument.position}:${argument.name}`}
164
+ direction="vertical"
165
+ size={6}
166
+ style={{ width: '100%' }}
167
+ >
168
+ <Typography.Text>
169
+ {argument.name}{' '}
170
+ <Typography.Text type="secondary">
171
+ ({argument.type})
172
+ </Typography.Text>
173
+ </Typography.Text>
174
+ <ParameterBindingEditor
175
+ name={argument.name}
176
+ defaultPath={defaultArgumentPath(argument.name)}
177
+ value={
178
+ config.argumentBindings?.[index] ?? defaultBinding(argument)
179
+ }
180
+ disabled={disabled}
181
+ onChange={(binding) => {
182
+ const argumentBindings = selected.arguments.map(
183
+ (item, bindingIndex) =>
184
+ config.argumentBindings?.[bindingIndex] ??
185
+ defaultBinding(item),
186
+ );
187
+ argumentBindings[index] = binding;
188
+ onChange({ ...config, argumentBindings });
189
+ }}
190
+ />
191
+ </Space>
192
+ ))
193
+ )}
194
+ </>
195
+ )}
123
196
 
124
197
  <Space direction="vertical" size={6} style={{ width: '100%' }}>
125
198
  <Typography.Text>{t('nodeEditors.database.timeout')}</Typography.Text>
@@ -396,7 +396,7 @@ export function EventTriggerConfigEditor({
396
396
  },
397
397
  });
398
398
  }}
399
- style={{ width: '45%' }}
399
+ style={{ width: '50%' }}
400
400
  />
401
401
  <Select<EventMatchOperator>
402
402
  aria-label={t('nodeEditors.event.conditionOperator', {
@@ -425,103 +425,103 @@ export function EventTriggerConfigEditor({
425
425
  },
426
426
  })
427
427
  }
428
- style={{ width: '30%' }}
428
+ style={{ width: '50%' }}
429
429
  />
430
- {!UNARY_MATCH_OPERATORS.has(condition.operator) &&
431
- (condition.operator === 'IN' ? (
432
- <Select
433
- mode="tags"
434
- aria-label={t('nodeEditors.event.conditionValue', {
435
- number: index + 1,
436
- })}
437
- value={parseValueList(condition.right?.value)}
438
- disabled={disabled}
439
- placeholder={t('nodeEditors.event.valueListHint')}
440
- onChange={(values) =>
441
- updateMatchCondition(index, {
442
- ...condition,
443
- right: {
444
- source: 'FIXED',
445
- valueType: fixedValueType(selectedField),
446
- value: JSON.stringify(values),
447
- },
448
- })
449
- }
450
- style={{ width: '25%' }}
451
- />
452
- ) : selectedField?.type === 'boolean' ? (
453
- <Select
454
- aria-label={t('nodeEditors.event.conditionValue', {
455
- number: index + 1,
456
- })}
457
- value={condition.right?.value || undefined}
458
- disabled={disabled}
459
- options={[
460
- {
461
- value: 'true',
462
- label: t('nodeEditors.event.booleanTrue'),
430
+ </Space.Compact>
431
+ {!UNARY_MATCH_OPERATORS.has(condition.operator) &&
432
+ (condition.operator === 'IN' ? (
433
+ <Select
434
+ mode="tags"
435
+ aria-label={t('nodeEditors.event.conditionValue', {
436
+ number: index + 1,
437
+ })}
438
+ value={parseValueList(condition.right?.value)}
439
+ disabled={disabled}
440
+ placeholder={t('nodeEditors.event.valueListHint')}
441
+ onChange={(values) =>
442
+ updateMatchCondition(index, {
443
+ ...condition,
444
+ right: {
445
+ source: 'FIXED',
446
+ valueType: fixedValueType(selectedField),
447
+ value: JSON.stringify(values),
463
448
  },
464
- {
465
- value: 'false',
466
- label: t('nodeEditors.event.booleanFalse'),
449
+ })
450
+ }
451
+ style={{ width: '100%' }}
452
+ />
453
+ ) : selectedField?.type === 'boolean' ? (
454
+ <Select
455
+ aria-label={t('nodeEditors.event.conditionValue', {
456
+ number: index + 1,
457
+ })}
458
+ value={condition.right?.value || undefined}
459
+ disabled={disabled}
460
+ options={[
461
+ {
462
+ value: 'true',
463
+ label: t('nodeEditors.event.booleanTrue'),
464
+ },
465
+ {
466
+ value: 'false',
467
+ label: t('nodeEditors.event.booleanFalse'),
468
+ },
469
+ ]}
470
+ onChange={(value) =>
471
+ updateMatchCondition(index, {
472
+ ...condition,
473
+ right: {
474
+ source: 'FIXED',
475
+ valueType: 'BOOLEAN',
476
+ value,
467
477
  },
468
- ]}
469
- onChange={(value) =>
470
- updateMatchCondition(index, {
471
- ...condition,
472
- right: {
473
- source: 'FIXED',
474
- valueType: 'BOOLEAN',
475
- value,
476
- },
477
- })
478
- }
479
- style={{ width: '25%' }}
480
- />
481
- ) : selectedField?.type === 'number' ? (
482
- <InputNumber
483
- aria-label={t('nodeEditors.event.conditionValue', {
484
- number: index + 1,
485
- })}
486
- value={
487
- condition.right?.value
488
- ? Number(condition.right.value)
489
- : null
490
- }
491
- disabled={disabled}
492
- onChange={(value) =>
493
- updateMatchCondition(index, {
494
- ...condition,
495
- right: {
496
- source: 'FIXED',
497
- valueType: 'NUMBER',
498
- value: value === null ? '' : String(value),
499
- },
500
- })
501
- }
502
- style={{ width: '25%' }}
503
- />
504
- ) : (
505
- <Input
506
- aria-label={t('nodeEditors.event.conditionValue', {
507
- number: index + 1,
508
- })}
509
- value={condition.right?.value ?? ''}
510
- disabled={disabled}
511
- onChange={(event) =>
512
- updateMatchCondition(index, {
513
- ...condition,
514
- right: {
515
- source: 'FIXED',
516
- valueType: 'STRING',
517
- value: event.target.value,
518
- },
519
- })
520
- }
521
- style={{ width: '25%' }}
522
- />
523
- ))}
524
- </Space.Compact>
478
+ })
479
+ }
480
+ style={{ width: '100%' }}
481
+ />
482
+ ) : selectedField?.type === 'number' ? (
483
+ <InputNumber
484
+ aria-label={t('nodeEditors.event.conditionValue', {
485
+ number: index + 1,
486
+ })}
487
+ value={
488
+ condition.right?.value
489
+ ? Number(condition.right.value)
490
+ : null
491
+ }
492
+ disabled={disabled}
493
+ onChange={(value) =>
494
+ updateMatchCondition(index, {
495
+ ...condition,
496
+ right: {
497
+ source: 'FIXED',
498
+ valueType: 'NUMBER',
499
+ value: value === null ? '' : String(value),
500
+ },
501
+ })
502
+ }
503
+ style={{ width: '100%' }}
504
+ />
505
+ ) : (
506
+ <Input
507
+ aria-label={t('nodeEditors.event.conditionValue', {
508
+ number: index + 1,
509
+ })}
510
+ value={condition.right?.value ?? ''}
511
+ disabled={disabled}
512
+ onChange={(event) =>
513
+ updateMatchCondition(index, {
514
+ ...condition,
515
+ right: {
516
+ source: 'FIXED',
517
+ valueType: 'STRING',
518
+ value: event.target.value,
519
+ },
520
+ })
521
+ }
522
+ style={{ width: '100%' }}
523
+ />
524
+ ))}
525
525
  <Button
526
526
  type="text"
527
527
  danger
@@ -0,0 +1,81 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { useState } from 'react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import i18n from '../../../../i18n';
5
+ import { EventTriggerLifecycleConfigEditor } from './EventTriggerLifecycleConfigEditor';
6
+
7
+ vi.mock('../../connections/connectionsApi', () => ({
8
+ fetchConnections: vi.fn(() => new Promise(() => undefined)),
9
+ }));
10
+
11
+ vi.mock('./EventTriggerConfigEditor', () => ({
12
+ EventTriggerConfigEditor: ({ onChange }: any) => (
13
+ <button
14
+ type="button"
15
+ onClick={() =>
16
+ onChange({
17
+ registrationCode: 'request',
18
+ operationRules: [{ operation: 'INSERT' }],
19
+ processingMode: 'PARALLEL',
20
+ })
21
+ }
22
+ >
23
+ change trigger
24
+ </button>
25
+ ),
26
+ }));
27
+
28
+ Object.defineProperty(window, 'matchMedia', {
29
+ writable: true,
30
+ value: () => ({
31
+ matches: false,
32
+ addListener() {},
33
+ removeListener() {},
34
+ addEventListener() {},
35
+ removeEventListener() {},
36
+ dispatchEvent: () => false,
37
+ }),
38
+ });
39
+
40
+ function Editor() {
41
+ const [value, setValue] = useState<Record<string, unknown>>({
42
+ registrationCode: 'request',
43
+ operationRules: [{ operation: 'INSERT' }],
44
+ processingMode: 'PARALLEL',
45
+ failureCallback: {
46
+ connectionRef: 'hardware-service',
47
+ path: '/api/hardware-disposal/workflow-failed',
48
+ },
49
+ });
50
+ return (
51
+ <>
52
+ <EventTriggerLifecycleConfigEditor
53
+ value={value}
54
+ onChange={(next) => setValue(next ?? {})}
55
+ schema={{ type: 'object' }}
56
+ />
57
+ <output data-testid="config">{JSON.stringify(value)}</output>
58
+ </>
59
+ );
60
+ }
61
+
62
+ describe('EventTriggerLifecycleConfigEditor', () => {
63
+ it('preserves the failure callback when the trigger editor updates its config', async () => {
64
+ await i18n.changeLanguage('zh_CN');
65
+ render(<Editor />);
66
+ await waitFor(() =>
67
+ expect(screen.getByText('Workflow 失败通知')).toBeInTheDocument(),
68
+ );
69
+
70
+ fireEvent.click(screen.getByText('change trigger'));
71
+
72
+ expect(JSON.parse(screen.getByTestId('config').textContent!)).toMatchObject(
73
+ {
74
+ failureCallback: {
75
+ connectionRef: 'hardware-service',
76
+ path: '/api/hardware-disposal/workflow-failed',
77
+ },
78
+ },
79
+ );
80
+ });
81
+ });