@gadmin2n/schematics 0.0.148 → 0.0.150
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.
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +66 -49
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +91 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +57 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +11 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +11 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +123 -60
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +31 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
- package/package.json +1 -1
|
@@ -61,7 +61,7 @@ export interface BusinessEventCatalogItem {
|
|
|
61
61
|
label: string;
|
|
62
62
|
schemaName: string;
|
|
63
63
|
tableName: string;
|
|
64
|
-
|
|
64
|
+
operations: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
|
|
65
65
|
watchedFields: string[];
|
|
66
66
|
primaryKeyFields: string[];
|
|
67
67
|
isEnabled: boolean;
|
|
@@ -128,7 +128,7 @@ export function buildBusinessEventCatalog(
|
|
|
128
128
|
code: string;
|
|
129
129
|
schemaName: string;
|
|
130
130
|
tableName: string;
|
|
131
|
-
|
|
131
|
+
operations: string[];
|
|
132
132
|
watchedFields: Prisma.JsonValue;
|
|
133
133
|
primaryKeyFields: Prisma.JsonValue;
|
|
134
134
|
isEnabled: boolean;
|
|
@@ -143,12 +143,14 @@ export function buildBusinessEventCatalog(
|
|
|
143
143
|
column.schemaName === row.schemaName &&
|
|
144
144
|
column.tableName === row.tableName,
|
|
145
145
|
);
|
|
146
|
-
const prefixes =
|
|
147
|
-
row.operation
|
|
148
|
-
? ['after']
|
|
149
|
-
: row.operation === 'DELETE'
|
|
146
|
+
const prefixes = [
|
|
147
|
+
...(row.operations.some((operation) => operation !== 'INSERT')
|
|
150
148
|
? ['before']
|
|
151
|
-
: [
|
|
149
|
+
: []),
|
|
150
|
+
...(row.operations.some((operation) => operation !== 'DELETE')
|
|
151
|
+
? ['after']
|
|
152
|
+
: []),
|
|
153
|
+
];
|
|
152
154
|
const fields = [
|
|
153
155
|
...primaryKeyFields.flatMap((columnName) => {
|
|
154
156
|
const column = sourceColumns.find(
|
|
@@ -164,17 +166,22 @@ export function buildBusinessEventCatalog(
|
|
|
164
166
|
`${prefix}.${column.columnName}`,
|
|
165
167
|
column,
|
|
166
168
|
false,
|
|
167
|
-
column.nullable
|
|
169
|
+
column.nullable ||
|
|
170
|
+
row.operations.includes(
|
|
171
|
+
prefix === 'before' ? 'INSERT' : 'DELETE',
|
|
172
|
+
),
|
|
168
173
|
),
|
|
169
174
|
),
|
|
170
175
|
),
|
|
171
176
|
];
|
|
172
177
|
return {
|
|
173
178
|
registrationCode: row.code,
|
|
174
|
-
label: `${row.schemaName}.${row.tableName} · ${row.
|
|
179
|
+
label: `${row.schemaName}.${row.tableName} · ${row.operations.join(
|
|
180
|
+
' / ',
|
|
181
|
+
)}`,
|
|
175
182
|
schemaName: row.schemaName,
|
|
176
183
|
tableName: row.tableName,
|
|
177
|
-
|
|
184
|
+
operations: row.operations as BusinessEventCatalogItem['operations'],
|
|
178
185
|
watchedFields,
|
|
179
186
|
primaryKeyFields,
|
|
180
187
|
isEnabled: row.isEnabled,
|
|
@@ -226,7 +233,28 @@ export function validateBusinessEventTriggers(
|
|
|
226
233
|
const event = eventByCode.get(registrationCode);
|
|
227
234
|
if (!event) continue;
|
|
228
235
|
|
|
229
|
-
|
|
236
|
+
const rules = config.operationRules;
|
|
237
|
+
if (!Array.isArray(rules) || rules.length === 0)
|
|
238
|
+
throw invalidBusinessEventMatch(event, '请至少启用一种操作的启动规则');
|
|
239
|
+
const seen = new Set<string>();
|
|
240
|
+
for (const value of rules) {
|
|
241
|
+
const rule = asRecord(value);
|
|
242
|
+
const operation =
|
|
243
|
+
rule.operation as BusinessEventCatalogItem['operations'][number];
|
|
244
|
+
if (!event.operations.includes(operation) || seen.has(operation))
|
|
245
|
+
throw invalidBusinessEventMatch(event, '操作未登记或重复配置');
|
|
246
|
+
seen.add(operation);
|
|
247
|
+
// before is unavailable for INSERT; after is unavailable for DELETE.
|
|
248
|
+
const availableEvent = {
|
|
249
|
+
...event,
|
|
250
|
+
fields: event.fields.filter(
|
|
251
|
+
(field) =>
|
|
252
|
+
!(operation === 'INSERT' && field.path.startsWith('before.')) &&
|
|
253
|
+
!(operation === 'DELETE' && field.path.startsWith('after.')),
|
|
254
|
+
),
|
|
255
|
+
};
|
|
256
|
+
validateBusinessEventMatch(rule.match, availableEvent);
|
|
257
|
+
}
|
|
230
258
|
|
|
231
259
|
if (config.processingMode !== 'SERIAL_BY_KEY') continue;
|
|
232
260
|
|
|
@@ -246,7 +274,13 @@ export function validateBusinessEventTriggers(
|
|
|
246
274
|
}
|
|
247
275
|
}
|
|
248
276
|
|
|
249
|
-
const EVENT_MATCH_OPERATORS = new Set([
|
|
277
|
+
const EVENT_MATCH_OPERATORS = new Set([
|
|
278
|
+
'EQ',
|
|
279
|
+
'NEQ',
|
|
280
|
+
'IN',
|
|
281
|
+
'EMPTY',
|
|
282
|
+
'NOT_EMPTY',
|
|
283
|
+
]);
|
|
250
284
|
const EVENT_MATCH_UNARY_OPERATORS = new Set(['EMPTY', 'NOT_EMPTY']);
|
|
251
285
|
|
|
252
286
|
function validateBusinessEventMatch(
|
|
@@ -292,14 +326,27 @@ function validateBusinessEventMatch(
|
|
|
292
326
|
: field.type === 'boolean'
|
|
293
327
|
? 'BOOLEAN'
|
|
294
328
|
: 'STRING';
|
|
329
|
+
let values: unknown[] = [right.value];
|
|
330
|
+
if (operator === 'IN') {
|
|
331
|
+
try {
|
|
332
|
+
values = JSON.parse(String(right.value));
|
|
333
|
+
} catch {
|
|
334
|
+
throw invalidBusinessEventMatch(event, '比较值列表无效');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
295
337
|
if (
|
|
296
338
|
right.source !== 'FIXED' ||
|
|
297
339
|
right.valueType !== expectedValueType ||
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
340
|
+
!Array.isArray(values) ||
|
|
341
|
+
values.length === 0 ||
|
|
342
|
+
values.some(
|
|
343
|
+
(value) =>
|
|
344
|
+
typeof value !== 'string' ||
|
|
345
|
+
(expectedValueType === 'NUMBER' &&
|
|
346
|
+
(!value.trim() || !Number.isFinite(Number(value)))) ||
|
|
347
|
+
(expectedValueType === 'BOOLEAN' &&
|
|
348
|
+
!['true', 'false'].includes(value)),
|
|
349
|
+
)
|
|
303
350
|
) {
|
|
304
351
|
throw invalidBusinessEventMatch(event, `字段“${path}”的比较值类型不正确`);
|
|
305
352
|
}
|
|
@@ -721,11 +768,7 @@ export class WorkflowService {
|
|
|
721
768
|
const [events, columns, schemaName] = await Promise.all([
|
|
722
769
|
this.prisma.workflowBusinessEventRegistration.findMany({
|
|
723
770
|
where: { isEnabled: true },
|
|
724
|
-
orderBy: [
|
|
725
|
-
{ schemaName: 'asc' },
|
|
726
|
-
{ tableName: 'asc' },
|
|
727
|
-
{ operation: 'asc' },
|
|
728
|
-
],
|
|
771
|
+
orderBy: [{ schemaName: 'asc' }, { tableName: 'asc' }, { code: 'asc' }],
|
|
729
772
|
}),
|
|
730
773
|
this.findBusinessEventColumns(),
|
|
731
774
|
this.findBusinessEventSchema(),
|
|
@@ -810,7 +853,7 @@ export class WorkflowService {
|
|
|
810
853
|
data: {
|
|
811
854
|
schemaName: string;
|
|
812
855
|
tableName: string;
|
|
813
|
-
|
|
856
|
+
operations: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
|
|
814
857
|
watchedFields: string[];
|
|
815
858
|
},
|
|
816
859
|
creator: string,
|
|
@@ -831,17 +874,24 @@ export class WorkflowService {
|
|
|
831
874
|
if (sourceColumns.length === 0)
|
|
832
875
|
throw new BadRequestException('请选择可监听的数据库表');
|
|
833
876
|
|
|
877
|
+
if (
|
|
878
|
+
!Array.isArray(data.operations) ||
|
|
879
|
+
data.operations.length === 0 ||
|
|
880
|
+
data.operations.some(
|
|
881
|
+
(operation) => !['INSERT', 'UPDATE', 'DELETE'].includes(operation),
|
|
882
|
+
)
|
|
883
|
+
) {
|
|
884
|
+
throw new BadRequestException('请至少选择一种有效的数据操作');
|
|
885
|
+
}
|
|
886
|
+
const operations = [...new Set(data.operations)].sort();
|
|
834
887
|
const requestedFields = [
|
|
835
888
|
...new Set(
|
|
836
889
|
data.watchedFields.map((field) => field.trim()).filter(Boolean),
|
|
837
890
|
),
|
|
838
891
|
].sort();
|
|
839
|
-
if (
|
|
892
|
+
if (operations.includes('UPDATE') && requestedFields.length === 0)
|
|
840
893
|
throw new BadRequestException('请至少选择一个监听字段');
|
|
841
|
-
const watchedFields =
|
|
842
|
-
data.operation === 'UPDATE'
|
|
843
|
-
? requestedFields
|
|
844
|
-
: sourceColumns.map((column) => column.columnName).sort();
|
|
894
|
+
const watchedFields = operations.includes('UPDATE') ? requestedFields : [];
|
|
845
895
|
const availableFields = new Set(
|
|
846
896
|
sourceColumns.map((column) => column.columnName),
|
|
847
897
|
);
|
|
@@ -861,7 +911,7 @@ export class WorkflowService {
|
|
|
861
911
|
JSON.stringify({
|
|
862
912
|
schemaName: data.schemaName,
|
|
863
913
|
tableName: data.tableName,
|
|
864
|
-
|
|
914
|
+
operations,
|
|
865
915
|
watchedFields,
|
|
866
916
|
}),
|
|
867
917
|
)
|
|
@@ -885,7 +935,7 @@ export class WorkflowService {
|
|
|
885
935
|
registrationKey,
|
|
886
936
|
schemaName: data.schemaName,
|
|
887
937
|
tableName: data.tableName,
|
|
888
|
-
|
|
938
|
+
operations,
|
|
889
939
|
watchedFields,
|
|
890
940
|
primaryKeyFields,
|
|
891
941
|
creator,
|
|
@@ -895,7 +945,9 @@ export class WorkflowService {
|
|
|
895
945
|
return registration;
|
|
896
946
|
});
|
|
897
947
|
this.logger.log(
|
|
898
|
-
`Registered ${event.
|
|
948
|
+
`Registered ${event.operations.join('/')} listener ${event.schemaName}.${
|
|
949
|
+
event.tableName
|
|
950
|
+
} (${event.code})`,
|
|
899
951
|
);
|
|
900
952
|
return buildBusinessEventCatalog([event], columns)[0];
|
|
901
953
|
}
|
|
@@ -906,7 +958,7 @@ export class WorkflowService {
|
|
|
906
958
|
id: bigint;
|
|
907
959
|
schemaName: string;
|
|
908
960
|
tableName: string;
|
|
909
|
-
|
|
961
|
+
operations: string[];
|
|
910
962
|
watchedFields: Prisma.JsonValue;
|
|
911
963
|
primaryKeyFields: Prisma.JsonValue;
|
|
912
964
|
},
|
|
@@ -934,10 +986,13 @@ export class WorkflowService {
|
|
|
934
986
|
const workflowSchema = quoteIdentifier(
|
|
935
987
|
workflowSchemaRows[0]?.schemaName || 'public',
|
|
936
988
|
);
|
|
937
|
-
const triggerEvent =
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
989
|
+
const triggerEvent = event.operations
|
|
990
|
+
.map((operation) =>
|
|
991
|
+
operation === 'UPDATE'
|
|
992
|
+
? `UPDATE OF ${watchedFields.map(quoteIdentifier).join(', ')}`
|
|
993
|
+
: operation,
|
|
994
|
+
)
|
|
995
|
+
.join(' OR ');
|
|
941
996
|
|
|
942
997
|
await tx.$executeRawUnsafe(`
|
|
943
998
|
CREATE OR REPLACE FUNCTION ${schema}.${functionName}()
|
|
@@ -353,3 +353,60 @@ describe('NodeConfigValidatorService', () => {
|
|
|
353
353
|
).not.toThrow();
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
|
+
|
|
357
|
+
describe('event trigger operation rules schema', () => {
|
|
358
|
+
const schema = NODE_TYPES.find((node) => node.type === 'event_trigger')!
|
|
359
|
+
.configSchema as JsonSchema;
|
|
360
|
+
const service = new NodeConfigValidatorService();
|
|
361
|
+
const validate = (operationRules: unknown) =>
|
|
362
|
+
service.validateOrThrow('event:5', schema, {
|
|
363
|
+
registrationCode: 'request-changes',
|
|
364
|
+
processingMode: 'PARALLEL',
|
|
365
|
+
operationRules,
|
|
366
|
+
});
|
|
367
|
+
it('accepts multiple operations with list conditions', () => {
|
|
368
|
+
expect(() =>
|
|
369
|
+
validate([
|
|
370
|
+
{ operation: 'INSERT' },
|
|
371
|
+
{
|
|
372
|
+
operation: 'UPDATE',
|
|
373
|
+
match: {
|
|
374
|
+
combineOperation: 'AND',
|
|
375
|
+
conditions: [
|
|
376
|
+
{
|
|
377
|
+
id: 'status',
|
|
378
|
+
left: { source: 'WORKFLOW', path: 'after.status' },
|
|
379
|
+
operator: 'IN',
|
|
380
|
+
right: {
|
|
381
|
+
source: 'FIXED',
|
|
382
|
+
valueType: 'STRING',
|
|
383
|
+
value: '["PENDING","PENDING_SUPERIOR"]',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
]),
|
|
390
|
+
).not.toThrow();
|
|
391
|
+
});
|
|
392
|
+
it('rejects empty rules and incomplete conditions', () => {
|
|
393
|
+
expect(() => validate([])).toThrow(BadRequestException);
|
|
394
|
+
expect(() =>
|
|
395
|
+
validate([
|
|
396
|
+
{
|
|
397
|
+
operation: 'UPDATE',
|
|
398
|
+
match: {
|
|
399
|
+
combineOperation: 'AND',
|
|
400
|
+
conditions: [
|
|
401
|
+
{
|
|
402
|
+
id: 'status',
|
|
403
|
+
left: { source: 'WORKFLOW', path: 'after.status' },
|
|
404
|
+
operator: 'IN',
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
]),
|
|
410
|
+
).toThrow(BadRequestException);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
@@ -371,7 +371,7 @@
|
|
|
371
371
|
"registerFailed": "Registration failed. Try again.",
|
|
372
372
|
"source": "Database table",
|
|
373
373
|
"operation": "Operation",
|
|
374
|
-
"watchedFields": "
|
|
374
|
+
"watchedFields": "Fields watched on update",
|
|
375
375
|
"allFields": "All fields",
|
|
376
376
|
"allFieldsHint": "INSERT and DELETE trigger whenever a row is inserted or deleted, and the event includes every field in that row.",
|
|
377
377
|
"sourceLoadFailed": "Database metadata could not be loaded",
|
|
@@ -552,7 +552,8 @@
|
|
|
552
552
|
"EQ": "Equals",
|
|
553
553
|
"NEQ": "Does not equal",
|
|
554
554
|
"EMPTY": "Is empty",
|
|
555
|
-
"NOT_EMPTY": "Is not empty"
|
|
555
|
+
"NOT_EMPTY": "Is not empty",
|
|
556
|
+
"IN": "Is one of"
|
|
556
557
|
},
|
|
557
558
|
"processing": "Event processing",
|
|
558
559
|
"parallel": "Process in parallel",
|
|
@@ -564,7 +565,14 @@
|
|
|
564
565
|
"primaryKey": "Primary key",
|
|
565
566
|
"sameKeyPreview": "Events with the same {{field}} are processed in arrival order",
|
|
566
567
|
"advanced": "Advanced settings",
|
|
567
|
-
"maxConcurrency": "Maximum concurrent objects"
|
|
568
|
+
"maxConcurrency": "Maximum concurrent objects",
|
|
569
|
+
"neverStart": "Do not start",
|
|
570
|
+
"valueListHint": "Enter a value and press Enter",
|
|
571
|
+
"operations": {
|
|
572
|
+
"INSERT": "On insert",
|
|
573
|
+
"UPDATE": "On update",
|
|
574
|
+
"DELETE": "On delete"
|
|
575
|
+
}
|
|
568
576
|
},
|
|
569
577
|
"database": {
|
|
570
578
|
"sql": "SQL statement",
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json
CHANGED
|
@@ -371,7 +371,7 @@
|
|
|
371
371
|
"registerFailed": "登记失败,请重试",
|
|
372
372
|
"source": "数据库表",
|
|
373
373
|
"operation": "数据操作",
|
|
374
|
-
"watchedFields": "
|
|
374
|
+
"watchedFields": "修改时监听字段",
|
|
375
375
|
"allFields": "全部字段",
|
|
376
376
|
"allFieldsHint": "INSERT 和 DELETE 会在任意一行插入或删除时触发,事件包含该行的全部字段。",
|
|
377
377
|
"sourceLoadFailed": "数据库表结构加载失败",
|
|
@@ -552,7 +552,8 @@
|
|
|
552
552
|
"EQ": "等于",
|
|
553
553
|
"NEQ": "不等于",
|
|
554
554
|
"EMPTY": "为空",
|
|
555
|
-
"NOT_EMPTY": "不为空"
|
|
555
|
+
"NOT_EMPTY": "不为空",
|
|
556
|
+
"IN": "属于以下任一值"
|
|
556
557
|
},
|
|
557
558
|
"processing": "事件处理方式",
|
|
558
559
|
"parallel": "并行处理",
|
|
@@ -564,7 +565,14 @@
|
|
|
564
565
|
"primaryKey": "主键",
|
|
565
566
|
"sameKeyPreview": "{{field}} 相同的事件会按到达顺序处理",
|
|
566
567
|
"advanced": "高级设置",
|
|
567
|
-
"maxConcurrency": "最大同时处理数"
|
|
568
|
+
"maxConcurrency": "最大同时处理数",
|
|
569
|
+
"neverStart": "不启动",
|
|
570
|
+
"valueListHint": "输入值并按回车添加",
|
|
571
|
+
"operations": {
|
|
572
|
+
"INSERT": "新增时",
|
|
573
|
+
"UPDATE": "修改时",
|
|
574
|
+
"DELETE": "删除时"
|
|
575
|
+
}
|
|
568
576
|
},
|
|
569
577
|
"database": {
|
|
570
578
|
"sql": "SQL 语句",
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Button,
|
|
7
7
|
Card,
|
|
8
8
|
Col,
|
|
9
|
+
DatePicker,
|
|
9
10
|
Input,
|
|
10
11
|
Modal,
|
|
11
12
|
Popconfirm,
|
|
@@ -19,6 +20,8 @@ import {
|
|
|
19
20
|
Typography,
|
|
20
21
|
message,
|
|
21
22
|
} from 'antd';
|
|
23
|
+
import type { Dayjs } from 'dayjs';
|
|
24
|
+
import dayjs from 'dayjs';
|
|
22
25
|
import {
|
|
23
26
|
DatabaseOutlined,
|
|
24
27
|
DeleteOutlined,
|
|
@@ -33,6 +36,7 @@ import { PageHeader } from 'components';
|
|
|
33
36
|
import { PAGE_WRAPPER_STYLE, SEARCH_CARD_PROPS } from 'constants/layout';
|
|
34
37
|
|
|
35
38
|
const { Text } = Typography;
|
|
39
|
+
const { RangePicker } = DatePicker;
|
|
36
40
|
|
|
37
41
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
38
42
|
|
|
@@ -180,6 +184,14 @@ export default function AgendaJobsPage() {
|
|
|
180
184
|
const page = Number(searchParams.get('page')) || 1;
|
|
181
185
|
const PAGE_SIZE = 20;
|
|
182
186
|
|
|
187
|
+
// 时间过滤(按 lastRunAt):默认最近 7 天,URL 无 from/to 时使用默认区间。
|
|
188
|
+
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs] | null>(() => {
|
|
189
|
+
const from = searchParams.get('from');
|
|
190
|
+
const to = searchParams.get('to');
|
|
191
|
+
if (from && to) return [dayjs(from), dayjs(to)];
|
|
192
|
+
return [dayjs().subtract(7, 'day').startOf('day'), dayjs().endOf('day')];
|
|
193
|
+
});
|
|
194
|
+
|
|
183
195
|
const updateParams = useCallback(
|
|
184
196
|
(updates: Record<string, string | undefined>) => {
|
|
185
197
|
setSearchParams(
|
|
@@ -217,6 +229,17 @@ export default function AgendaJobsPage() {
|
|
|
217
229
|
(p: number) => updateParams({ page: p > 1 ? String(p) : undefined }),
|
|
218
230
|
[updateParams],
|
|
219
231
|
);
|
|
232
|
+
const setDateRangeAndSync = useCallback(
|
|
233
|
+
(range: [Dayjs, Dayjs] | null) => {
|
|
234
|
+
setDateRange(range);
|
|
235
|
+
updateParams({
|
|
236
|
+
from: range?.[0]?.toISOString(),
|
|
237
|
+
to: range?.[1]?.toISOString(),
|
|
238
|
+
page: undefined,
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
[updateParams],
|
|
242
|
+
);
|
|
220
243
|
|
|
221
244
|
// Auto-refresh
|
|
222
245
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -232,6 +255,8 @@ export default function AgendaJobsPage() {
|
|
|
232
255
|
if (filterToggle) params.toggle = filterToggle;
|
|
233
256
|
if (filterName) params.q = filterName;
|
|
234
257
|
if (filterModule) params.module = filterModule;
|
|
258
|
+
if (dateRange?.[0]) params.dateFrom = dateRange[0].toISOString();
|
|
259
|
+
if (dateRange?.[1]) params.dateTo = dateRange[1].toISOString();
|
|
235
260
|
|
|
236
261
|
const [res, metrics] = await Promise.all([
|
|
237
262
|
customRequest<JobsResponse>('agenda/jobs', 'GET', params),
|
|
@@ -247,7 +272,7 @@ export default function AgendaJobsPage() {
|
|
|
247
272
|
} finally {
|
|
248
273
|
setLoading(false);
|
|
249
274
|
}
|
|
250
|
-
}, [page, filterState, filterToggle, filterName, filterModule]);
|
|
275
|
+
}, [page, filterState, filterToggle, filterName, filterModule, dateRange]);
|
|
251
276
|
|
|
252
277
|
useEffect(() => {
|
|
253
278
|
fetchJobs();
|
|
@@ -715,6 +740,21 @@ export default function AgendaJobsPage() {
|
|
|
715
740
|
style={{ width: 160 }}
|
|
716
741
|
options={modules.map((m) => ({ value: m, label: m }))}
|
|
717
742
|
/>
|
|
743
|
+
<RangePicker
|
|
744
|
+
prefix={
|
|
745
|
+
<span
|
|
746
|
+
style={{ color: 'var(--ant-color-text)', whiteSpace: 'nowrap' }}
|
|
747
|
+
>
|
|
748
|
+
最近执行
|
|
749
|
+
</span>
|
|
750
|
+
}
|
|
751
|
+
value={dateRange}
|
|
752
|
+
onChange={(range) =>
|
|
753
|
+
setDateRangeAndSync(range as [Dayjs, Dayjs] | null)
|
|
754
|
+
}
|
|
755
|
+
allowClear
|
|
756
|
+
style={{ width: 300 }}
|
|
757
|
+
/>
|
|
718
758
|
<Input
|
|
719
759
|
placeholder="按名称过滤"
|
|
720
760
|
allowClear
|
|
@@ -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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
watchedFields:
|
|
70
|
-
|
|
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={{
|
|
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="
|
|
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
|
|
139
|
+
if (!value.includes('UPDATE'))
|
|
140
|
+
form.setFieldValue('watchedFields', []);
|
|
137
141
|
}}
|
|
138
142
|
/>
|
|
139
143
|
</Form.Item>
|
|
140
|
-
{
|
|
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
|
-
|
|
36
|
+
operations: ['UPDATE'],
|
|
37
37
|
watchedFields: ['status'],
|
|
38
38
|
primaryKeyFields: ['id'],
|
|
39
39
|
isEnabled: true,
|
|
@@ -257,7 +257,8 @@ export default function BusinessEventDetailPage() {
|
|
|
257
257
|
label={t('eventCatalog.identifier')}
|
|
258
258
|
value={
|
|
259
259
|
<Typography.Text code>
|
|
260
|
-
{event.schemaName}.{event.tableName} ·
|
|
260
|
+
{event.schemaName}.{event.tableName} ·{' '}
|
|
261
|
+
{event.operations.join(' / ')}
|
|
261
262
|
</Typography.Text>
|
|
262
263
|
}
|
|
263
264
|
/>
|
|
@@ -38,7 +38,7 @@ describe('BusinessEventCatalogPage', () => {
|
|
|
38
38
|
label: 'public.t_hardware_asset · UPDATE',
|
|
39
39
|
schemaName: 'public',
|
|
40
40
|
tableName: 't_hardware_asset',
|
|
41
|
-
|
|
41
|
+
operations: ['UPDATE'],
|
|
42
42
|
watchedFields: ['status'],
|
|
43
43
|
primaryKeyFields: ['id'],
|
|
44
44
|
isEnabled: true,
|