@gadmin2n/schematics 0.0.147 → 0.0.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +2 -1
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +83 -53
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +4 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +106 -37
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +59 -3
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +60 -14
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +60 -14
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +124 -62
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +78 -17
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  104. package/package.json +1 -1
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
@@ -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} · {event.operation}
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
- operation: 'UPDATE',
41
+ operations: ['UPDATE'],
42
42
  watchedFields: ['status'],
43
43
  primaryKeyFields: ['id'],
44
44
  isEnabled: true,
@@ -74,7 +74,7 @@ export default function BusinessEventCatalogPage() {
74
74
  event.label,
75
75
  event.schemaName,
76
76
  event.tableName,
77
- event.operation,
77
+ ...event.operations,
78
78
  ...event.fields.flatMap((field) => [
79
79
  field.path,
80
80
  field.label,
@@ -105,7 +105,7 @@ export default function BusinessEventCatalogPage() {
105
105
  {event.label}
106
106
  </Typography.Link>
107
107
  <Typography.Text code>
108
- {event.operation === 'UPDATE'
108
+ {event.operations.includes('UPDATE')
109
109
  ? event.watchedFields.join(', ')
110
110
  : t('eventCatalog.registration.allFields')}
111
111
  </Typography.Text>
@@ -83,6 +83,13 @@ describe('NodeOutputSummary', () => {
83
83
  type: 'object',
84
84
  properties: {
85
85
  operation: { type: 'string' },
86
+ table: {
87
+ type: 'object',
88
+ properties: {
89
+ schema: { type: 'string' },
90
+ name: { type: 'string' },
91
+ },
92
+ },
86
93
  primaryKey: { type: 'object' },
87
94
  before: { type: ['object', 'null'] },
88
95
  after: { type: ['object', 'null'] },
@@ -90,6 +97,7 @@ describe('NodeOutputSummary', () => {
90
97
  },
91
98
  required: [
92
99
  'operation',
100
+ 'table',
93
101
  'primaryKey',
94
102
  'before',
95
103
  'after',
@@ -102,8 +110,20 @@ describe('NodeOutputSummary', () => {
102
110
  render(<NodeOutputSummary node={event} nodeType={nodeType} />);
103
111
 
104
112
  expect(screen.getByText('input.operation')).toBeInTheDocument();
113
+ expect(screen.getByText('操作类型')).toBeInTheDocument();
114
+ expect(
115
+ screen.getByText(
116
+ '数据库操作类型:INSERT(新增)、UPDATE(更新)或 DELETE(删除)',
117
+ ),
118
+ ).toBeInTheDocument();
119
+ expect(screen.getByText('input.table')).toBeInTheDocument();
120
+ expect(screen.getByText('数据表')).toBeInTheDocument();
105
121
  expect(screen.getByText('input.primaryKey')).toBeInTheDocument();
122
+ expect(screen.getByText('主键')).toBeInTheDocument();
123
+ expect(screen.getByText('变更前的数据')).toBeInTheDocument();
124
+ expect(screen.getByText('变更后的数据')).toBeInTheDocument();
106
125
  expect(screen.getByText('input.changedFields')).toBeInTheDocument();
126
+ expect(screen.getByText('发生变化的字段')).toBeInTheDocument();
107
127
  expect(
108
128
  screen.queryByText('触发本次运行的事件数据'),
109
129
  ).not.toBeInTheDocument();
@@ -188,11 +188,10 @@ describe('NodePropertyPanel instance references', () => {
188
188
  expect(
189
189
  screen.queryByText('Connection is required'),
190
190
  ).not.toBeInTheDocument();
191
- await user.click(screen.getByRole('button', { name: 'Save changes' }));
192
- expect(onSave).toHaveBeenCalledWith(
193
- 'http-1',
194
- expect.objectContaining({ config: expect.any(Object) }),
195
- );
191
+ expect(
192
+ screen.queryByRole('button', { name: 'Save changes' }),
193
+ ).not.toBeInTheDocument();
194
+ expect(onSave).not.toHaveBeenCalled();
196
195
  expect(
197
196
  screen.queryByText('Connection is required'),
198
197
  ).not.toBeInTheDocument();
@@ -307,7 +306,6 @@ describe('NodePropertyPanel instance references', () => {
307
306
  />,
308
307
  );
309
308
 
310
- await waitFor(() => expect(customRequest).toHaveBeenCalled());
311
309
  await act(async () => {
312
310
  await Promise.resolve();
313
311
  await Promise.resolve();
@@ -346,8 +344,8 @@ describe('NodePropertyPanel instance references', () => {
346
344
  screen.getByText('Includes the approver and completion time'),
347
345
  ).toBeInTheDocument();
348
346
  expect(
349
- screen.getByRole('button', { name: 'Save changes' }),
350
- ).toBeInTheDocument();
347
+ screen.queryByRole('button', { name: 'Save changes' }),
348
+ ).not.toBeInTheDocument();
351
349
  expect(customRequest).not.toHaveBeenCalledWith(
352
350
  'workflowNodeInstance/findMany',
353
351
  expect.anything(),
@@ -386,13 +384,14 @@ describe('NodePropertyPanel instance references', () => {
386
384
  />,
387
385
  );
388
386
 
389
- expect(screen.getByRole('button', { name: 'Save changes' })).toBeEnabled();
390
387
  expect(
391
388
  screen.getByText('Use a template or configure this node directly'),
392
389
  ).toBeInTheDocument();
393
- expect(screen.getByRole('button', { name: /New template/ })).toHaveClass(
394
- 'workflow-panel-action',
395
- );
390
+ expect(
391
+ screen.getByRole('button', {
392
+ name: /Save current configuration as template/,
393
+ }),
394
+ ).toHaveClass('workflow-panel-action');
396
395
  expect(screen.getByText('Who can approve')).toBeInTheDocument();
397
396
  expect(screen.queryByText('Parameters')).not.toBeInTheDocument();
398
397
  expect(
@@ -419,7 +418,83 @@ describe('NodePropertyPanel instance references', () => {
419
418
  );
420
419
  });
421
420
 
422
- it('keeps an inline approval configuration after saving it', async () => {
421
+ it('saves the current configuration as a template and uses it immediately', async () => {
422
+ const user = userEvent.setup();
423
+ const onSave = vi.fn();
424
+ const inlineNode = {
425
+ ...node,
426
+ instanceRef: undefined,
427
+ config: { subjects: [{ refs: ['alice'] }] },
428
+ };
429
+ vi.mocked(customRequest).mockImplementation((path) => {
430
+ if (path === 'workflowNodeInstance/findMany') {
431
+ return Promise.resolve({ data: [] }) as never;
432
+ }
433
+ if (path === 'workflowNodeInstance/createOne') {
434
+ return Promise.resolve({
435
+ id: '21',
436
+ code: 'FINANCE_APPROVAL_V2',
437
+ name: 'Finance approval',
438
+ nodeTypeId: '7',
439
+ isEnabled: true,
440
+ config: inlineNode.config,
441
+ }) as never;
442
+ }
443
+ return Promise.resolve([]) as never;
444
+ });
445
+
446
+ render(
447
+ <NodePropertyPanel
448
+ node={inlineNode}
449
+ nodeTypes={[nodeType]}
450
+ onSave={onSave}
451
+ onLabelChange={vi.fn()}
452
+ onAiEdit={vi.fn()}
453
+ aiLoading={false}
454
+ />,
455
+ );
456
+
457
+ await user.click(
458
+ screen.getByRole('button', {
459
+ name: /Save current configuration as template/,
460
+ }),
461
+ );
462
+ expect(
463
+ screen.getByText(/Create a template from the current node configuration/),
464
+ ).toBeInTheDocument();
465
+ const templateName = screen.getByRole('textbox', {
466
+ name: 'Template name',
467
+ });
468
+ await user.clear(templateName);
469
+ await user.type(templateName, 'Reusable finance approval');
470
+ await user.click(
471
+ screen.getByRole('button', { name: 'Save and use template' }),
472
+ );
473
+
474
+ await waitFor(() =>
475
+ expect(customRequest).toHaveBeenCalledWith(
476
+ 'workflowNodeInstance/createOne',
477
+ 'POST',
478
+ expect.objectContaining({
479
+ name: 'Reusable finance approval',
480
+ config: inlineNode.config,
481
+ }),
482
+ ),
483
+ );
484
+ expect(onSave).toHaveBeenCalledWith(
485
+ 'node-1',
486
+ expect.objectContaining({
487
+ config: inlineNode.config,
488
+ configOverride: null,
489
+ instanceRef: {
490
+ instanceId: '21',
491
+ instanceCode: 'FINANCE_APPROVAL_V2',
492
+ },
493
+ }),
494
+ );
495
+ });
496
+
497
+ it('keeps an inline approval configuration in the Workflow draft', async () => {
423
498
  const user = userEvent.setup();
424
499
  const onSave = vi.fn();
425
500
  const onConfigDraftChange = vi.fn();
@@ -531,10 +606,8 @@ describe('NodePropertyPanel instance references', () => {
531
606
  },
532
607
  });
533
608
 
534
- await user.click(screen.getByRole('button', { name: 'Save changes' }));
535
-
536
- const savedConfig = onSave.mock.lastCall?.[1].config;
537
- expect(savedConfig).toMatchObject({
609
+ const pendingConfig = panelRef.current?.getPendingUpdate()?.updates.config;
610
+ expect(pendingConfig).toMatchObject({
538
611
  external: {
539
612
  request: {
540
613
  connectionRef: 'ticketing',
@@ -549,6 +622,7 @@ describe('NodePropertyPanel instance references', () => {
549
622
  rejectionRule: { mode: 'ANY' },
550
623
  timeout: { durationSeconds: 0, action: 'REJECTED' },
551
624
  });
625
+ expect(onSave).not.toHaveBeenCalled();
552
626
 
553
627
  await waitFor(() =>
554
628
  expect(screen.getByLabelText('Task title field path')).toHaveValue(
@@ -616,7 +690,7 @@ describe('NodePropertyPanel instance references', () => {
616
690
 
617
691
  it('uses nested UI Schema widgets for Callable Trigger parameters', async () => {
618
692
  const user = userEvent.setup();
619
- const onSave = vi.fn();
693
+ const onConfigDraftChange = vi.fn();
620
694
  const callableNodeType = {
621
695
  id: '9',
622
696
  type: 'callable_trigger',
@@ -655,7 +729,8 @@ describe('NodePropertyPanel instance references', () => {
655
729
  <NodePropertyPanel
656
730
  node={callableNode}
657
731
  nodeTypes={[callableNodeType]}
658
- onSave={onSave}
732
+ onSave={vi.fn()}
733
+ onConfigDraftChange={onConfigDraftChange}
659
734
  onLabelChange={vi.fn()}
660
735
  onAiEdit={vi.fn()}
661
736
  aiLoading={false}
@@ -672,16 +747,10 @@ describe('NodePropertyPanel instance references', () => {
672
747
  screen.getByRole('textbox', { name: 'Parameter name' }),
673
748
  'departmentId',
674
749
  );
675
- await user.click(screen.getByRole('button', { name: 'Save changes' }));
676
750
 
677
- expect(onSave).toHaveBeenCalledWith(
678
- 'callable-1',
679
- expect.objectContaining({
680
- config: {
681
- inputParameters: [{ key: 'departmentId', label: '', required: true }],
682
- },
683
- }),
684
- );
751
+ expect(onConfigDraftChange).toHaveBeenCalledWith('callable-1', {
752
+ inputParameters: [{ key: 'departmentId', label: '', required: true }],
753
+ });
685
754
  });
686
755
 
687
756
  it('filters the node palette by translated name and technical type', async () => {
@@ -2,6 +2,7 @@ import {
2
2
  DisconnectOutlined,
3
3
  MinusCircleOutlined,
4
4
  PlusOutlined,
5
+ SaveOutlined,
5
6
  SearchOutlined,
6
7
  SendOutlined,
7
8
  } from '@ant-design/icons';
@@ -9,11 +10,11 @@ import {
9
10
  Alert,
10
11
  Button,
11
12
  Card,
12
- Checkbox,
13
13
  Form,
14
14
  Input,
15
15
  InputNumber,
16
16
  message,
17
+ Modal,
17
18
  Segmented,
18
19
  Select,
19
20
  Space,
@@ -44,7 +45,6 @@ import type {
44
45
  WorkflowNodeType,
45
46
  } from '../types';
46
47
  import { NodeOutputSummary } from './NodeOutputSummary';
47
- import { CreateNodeInstanceDrawer } from './CreateNodeInstanceDrawer';
48
48
  import { WorkflowCodeEditor } from './WorkflowCodeEditor';
49
49
  import '../node-instances/node-config.css';
50
50
  import type { ReferencedConfigStatus } from '../hooks/useReferencedNodeInstanceConfigs';
@@ -186,12 +186,11 @@ function NodePropertyPanelImpl(
186
186
  const [instances, setInstances] = useState<WorkflowNodeInstance[]>([]);
187
187
  const [instanceLoading, setInstanceLoading] = useState(false);
188
188
  const [instanceLoadError, setInstanceLoadError] = useState(false);
189
- const [createInstanceOpen, setCreateInstanceOpen] = useState(false);
189
+ const [saveTemplateOpen, setSaveTemplateOpen] = useState(false);
190
190
  const [paletteQuery, setPaletteQuery] = useState('');
191
- const [newInstanceName, setNewInstanceName] = useState('');
192
- const [newInstanceDesc, setNewInstanceDesc] = useState('');
193
- const [saveAsLoading, setSaveAsLoading] = useState(false);
194
- const [alsoSaveAsInstance, setAlsoSaveAsInstance] = useState(false);
191
+ const [templateName, setTemplateName] = useState('');
192
+ const [templateDescription, setTemplateDescription] = useState('');
193
+ const [templateSaving, setTemplateSaving] = useState(false);
195
194
  const [configForm] = Form.useForm();
196
195
 
197
196
  // Find configSchema for current node type
@@ -326,7 +325,6 @@ function NodePropertyPanelImpl(
326
325
  ]);
327
326
  setDirty(false);
328
327
  setPrompt('');
329
- setAlsoSaveAsInstance(false);
330
328
  }
331
329
  }, [node?.id, currentNodeType]);
332
330
 
@@ -414,8 +412,7 @@ function NodePropertyPanelImpl(
414
412
 
415
413
  // Reset selection when node changes
416
414
  useEffect(() => {
417
- setAlsoSaveAsInstance(false);
418
- setCreateInstanceOpen(false);
415
+ setSaveTemplateOpen(false);
419
416
  }, [node?.id]);
420
417
 
421
418
  const buildCurrentUpdates = useCallback((): NodePropertyUpdates | null => {
@@ -590,57 +587,51 @@ function NodePropertyPanelImpl(
590
587
  }
591
588
  }
592
589
 
593
- async function handleSave() {
594
- if (!node) return;
590
+ function openSaveTemplate() {
591
+ if (!node || node.instanceRef) return;
592
+ setTemplateName(node.label.trim());
593
+ setTemplateDescription('');
594
+ setSaveTemplateOpen(true);
595
+ }
596
+
597
+ async function handleSaveAsTemplate() {
598
+ if (!node || node.instanceRef || !currentNodeType || !templateName.trim())
599
+ return;
595
600
  const updates = buildCurrentUpdates();
596
601
  if (!updates?.config) return;
597
- const cleanConfig = updates.config;
598
- if (
599
- !node.instanceRef &&
600
- alsoSaveAsInstance &&
601
- currentNodeType &&
602
- newInstanceName.trim()
603
- ) {
604
- // Save node AND create a new instance, then reference it
605
- setSaveAsLoading(true);
606
- try {
607
- const res = await customRequest<any>(
608
- 'workflowNodeInstance/createOne',
609
- 'POST',
610
- {
611
- nodeTypeId: Number(currentNodeType.id),
612
- name: newInstanceName.trim(),
613
- description: newInstanceDesc.trim() || null,
614
- config: cleanConfig,
615
- isEnabled: true,
616
- },
617
- );
618
- const created = res;
619
- const newId = String(created.id);
620
- onSave(node.id, {
621
- label,
622
- config: created.config,
623
- configOverride: null,
624
- instanceRef: {
625
- instanceId: newId,
626
- instanceCode: created.code,
627
- },
628
- });
629
- setInstances((prev) => [...prev, created]);
630
- setAlsoSaveAsInstance(false);
631
- setNewInstanceName('');
632
- setNewInstanceDesc('');
633
- message.success('已保存并创建新实例');
634
- } catch (e: any) {
635
- message.error(e?.message ?? '创建实例失败');
636
- } finally {
637
- setSaveAsLoading(false);
638
- }
639
- } else {
640
- onSave(node.id, updates);
641
- message.success('节点已保存');
602
+ setTemplateSaving(true);
603
+ try {
604
+ const created = await customRequest<WorkflowNodeInstance>(
605
+ 'workflowNodeInstance/createOne',
606
+ 'POST',
607
+ {
608
+ nodeTypeId: Number(currentNodeType.id),
609
+ name: templateName.trim(),
610
+ description: templateDescription.trim() || null,
611
+ config: updates.config,
612
+ isEnabled: true,
613
+ },
614
+ );
615
+ onSave(node.id, {
616
+ label,
617
+ config: created.config,
618
+ configOverride: null,
619
+ instanceRef: {
620
+ instanceId: String(created.id),
621
+ instanceCode: created.code,
622
+ },
623
+ });
624
+ setInstances((current) => [
625
+ created,
626
+ ...current.filter((item) => item.id !== created.id),
627
+ ]);
628
+ setSaveTemplateOpen(false);
629
+ message.success(t('editor.templateSavedAndApplied'));
630
+ } catch (error: any) {
631
+ message.error(error?.message ?? t('editor.templateSaveFailed'));
632
+ } finally {
633
+ setTemplateSaving(false);
642
634
  }
643
- setDirty(false);
644
635
  }
645
636
 
646
637
  function handleAiSubmit() {
@@ -1017,10 +1008,11 @@ function NodePropertyPanelImpl(
1017
1008
  <Button
1018
1009
  size="small"
1019
1010
  className="workflow-panel-action"
1020
- icon={<PlusOutlined />}
1021
- onClick={() => setCreateInstanceOpen(true)}
1011
+ icon={<SaveOutlined />}
1012
+ disabled={referencedConfigUnavailable}
1013
+ onClick={openSaveTemplate}
1022
1014
  >
1023
- {t('editor.createTemplate')}
1015
+ {t('editor.saveCurrentAsTemplate')}
1024
1016
  </Button>
1025
1017
  </div>
1026
1018
  <Select
@@ -1225,64 +1217,40 @@ function NodePropertyPanelImpl(
1225
1217
  </Card>
1226
1218
  )}
1227
1219
  </div>
1228
- {activeTab === 'config' && (
1229
- <footer className="workflow-node-panel__footer">
1230
- {!node.instanceRef && (
1231
- <div className="workflow-node-panel__save-template">
1232
- <Checkbox
1233
- checked={alsoSaveAsInstance}
1234
- onChange={(e) => setAlsoSaveAsInstance(e.target.checked)}
1235
- >
1236
- <Text>{t('editor.saveAsInstance')}</Text>
1237
- </Checkbox>
1238
- {alsoSaveAsInstance && (
1239
- <Space direction="vertical" size={6} style={{ width: '100%' }}>
1240
- <Input
1241
- value={newInstanceName}
1242
- onChange={(e) => setNewInstanceName(e.target.value)}
1243
- placeholder={t('editor.instanceNamePlaceholder')}
1244
- size="small"
1245
- />
1246
- <Input
1247
- value={newInstanceDesc}
1248
- onChange={(e) => setNewInstanceDesc(e.target.value)}
1249
- placeholder={t('common.optionalDescription')}
1250
- size="small"
1251
- />
1252
- </Space>
1253
- )}
1254
- </div>
1255
- )}
1256
- <Button
1257
- type="primary"
1258
- size="large"
1259
- block
1260
- loading={saveAsLoading}
1261
- disabled={
1262
- referencedConfigUnavailable ||
1263
- (!node.instanceRef &&
1264
- alsoSaveAsInstance &&
1265
- !newInstanceName.trim())
1266
- }
1267
- onClick={handleSave}
1268
- >
1269
- {t('editor.saveChanges')}
1270
- </Button>
1271
- </footer>
1272
- )}
1273
- <CreateNodeInstanceDrawer
1274
- open={createInstanceOpen}
1275
- nodeType={currentNodeType}
1276
- onCancel={() => setCreateInstanceOpen(false)}
1277
- onCreated={(instance) => {
1278
- setCreateInstanceOpen(false);
1279
- setInstances((current) => [
1280
- instance,
1281
- ...current.filter((item) => item.id !== instance.id),
1282
- ]);
1283
- referenceInstance(instance);
1284
- }}
1285
- />
1220
+ <Modal
1221
+ open={saveTemplateOpen}
1222
+ title={t('editor.saveCurrentAsTemplate')}
1223
+ okText={t('editor.saveAndUseTemplate')}
1224
+ cancelText={t('common.cancel')}
1225
+ confirmLoading={templateSaving}
1226
+ okButtonProps={{ disabled: !templateName.trim() }}
1227
+ onOk={() => void handleSaveAsTemplate()}
1228
+ onCancel={() => setSaveTemplateOpen(false)}
1229
+ destroyOnHidden
1230
+ >
1231
+ <Typography.Paragraph type="secondary">
1232
+ {t('editor.saveCurrentAsTemplateHint')}
1233
+ </Typography.Paragraph>
1234
+ <Form layout="vertical">
1235
+ <Form.Item label={t('editor.templateName')} required>
1236
+ <Input
1237
+ aria-label={t('editor.templateName')}
1238
+ value={templateName}
1239
+ onChange={(event) => setTemplateName(event.target.value)}
1240
+ autoFocus
1241
+ />
1242
+ </Form.Item>
1243
+ <Form.Item label={t('common.description')}>
1244
+ <Input.TextArea
1245
+ aria-label={t('common.description')}
1246
+ value={templateDescription}
1247
+ onChange={(event) => setTemplateDescription(event.target.value)}
1248
+ placeholder={t('common.optionalDescription')}
1249
+ rows={3}
1250
+ />
1251
+ </Form.Item>
1252
+ </Form>
1253
+ </Modal>
1286
1254
  </div>
1287
1255
  );
1288
1256
  }
@@ -5,14 +5,19 @@ import { WorkflowCodeEditor } from './WorkflowCodeEditor';
5
5
 
6
6
  vi.mock('@monaco-editor/react', () => ({
7
7
  default: ({
8
+ language,
8
9
  value,
9
10
  onChange,
11
+ options,
10
12
  }: {
13
+ language?: string;
11
14
  value?: string;
12
15
  onChange?(value: string): void;
16
+ options?: { ariaLabel?: string };
13
17
  }) => (
14
18
  <textarea
15
- aria-label="shared editor"
19
+ aria-label={options?.ariaLabel ?? 'shared editor'}
20
+ data-language={language}
16
21
  value={value}
17
22
  onChange={(event) => onChange?.(event.target.value)}
18
23
  />
@@ -39,4 +44,20 @@ describe('WorkflowCodeEditor', () => {
39
44
  });
40
45
  expect(onChange).toHaveBeenCalledWith('{"ok":true}');
41
46
  });
47
+
48
+ it('enables Monaco SQL mode and labels the status bar', () => {
49
+ render(
50
+ <WorkflowCodeEditor
51
+ language="sql"
52
+ value="SELECT * FROM users"
53
+ ariaLabel="SQL 语句"
54
+ />,
55
+ );
56
+
57
+ expect(screen.getByRole('textbox', { name: 'SQL 语句' })).toHaveAttribute(
58
+ 'data-language',
59
+ 'sql',
60
+ );
61
+ expect(screen.getByText('SQL')).toBeInTheDocument();
62
+ });
42
63
  });
@@ -2,7 +2,13 @@ import Editor from '@monaco-editor/react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import './workflow-code-editor.css';
4
4
 
5
- type WorkflowEditorLanguage = 'json' | 'javascript';
5
+ type WorkflowEditorLanguage = 'json' | 'javascript' | 'sql';
6
+
7
+ const LANGUAGE_LABELS: Record<WorkflowEditorLanguage, string> = {
8
+ json: 'JSON',
9
+ javascript: 'JavaScript',
10
+ sql: 'SQL',
11
+ };
6
12
 
7
13
  export function WorkflowCodeEditor({
8
14
  language,
@@ -67,7 +73,7 @@ export function WorkflowCodeEditor({
67
73
  }}
68
74
  />
69
75
  <div className="workflow-code-editor__status" aria-hidden="true">
70
- <span>{language === 'json' ? 'JSON' : 'JavaScript'}</span>
76
+ <span>{LANGUAGE_LABELS[language]}</span>
71
77
  <span>UTF-8</span>
72
78
  <span>{t('editor.indentSpaces')}: 2</span>
73
79
  </div>