@steedos-labs/plugin-workflow 3.0.43 → 3.0.45

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 (29) hide show
  1. package/designer/dist/amis-renderer/amis-renderer.css +1 -1
  2. package/designer/dist/amis-renderer/amis-renderer.js +1 -1
  3. package/designer/dist/assets/{index-C3K_lyOl.css → index-B5zmn1Wt.css} +1 -1
  4. package/designer/dist/assets/{index-XZWBSnmF.js → index-D7W5yEF4.js} +268 -268
  5. package/designer/dist/index.html +3 -3
  6. package/main/default/objects/instance_tasks/buttons/instance_new.button.yml +1 -1
  7. package/main/default/objects/instances/buttons/instance_new.button.yml +1 -1
  8. package/main/default/objects/workflow_designer_backups.object.yml +5 -0
  9. package/main/default/pages/flowdetail.page.amis.json +0 -31
  10. package/main/default/routes/am.router.js +1 -2
  11. package/main/default/routes/api_workflow_ai_form_design_stream.router.js +36 -1
  12. package/main/default/routes/api_workflow_next_step.router.js +8 -1
  13. package/main/default/routes/api_workflow_next_step_users.router.js +4 -1
  14. package/main/default/routes/api_workflow_submit.router.js +31 -2
  15. package/main/default/routes/flow_form_design.ejs +2 -1
  16. package/main/default/routes/object_workflows.router.js +3 -3
  17. package/main/default/test/test_migrateApprovalCommentsField.js +220 -0
  18. package/main/default/test/test_rollbackApprovalCommentsField.js +235 -0
  19. package/main/default/triggers/amis_form_design.trigger.js +2 -1
  20. package/main/default/utils/designerManager.js +2 -5
  21. package/package.json +1 -1
  22. package/public/amis-renderer/amis-renderer.css +1 -1
  23. package/public/amis-renderer/amis-renderer.js +1 -1
  24. package/public/workflow/index.css +24 -1
  25. package/src/rests/approvalCommentsConsole.js +9 -0
  26. package/src/rests/getPageSchema.js +58 -0
  27. package/src/rests/index.js +1 -0
  28. package/src/rests/migrateApprovalCommentsField.js +31 -12
  29. package/src/rests/rollbackApprovalCommentsField.js +32 -21
@@ -2,11 +2,11 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/api/workflow/designer-v2/vite.svg" />
5
+ <link rel="shortcut icon" type="image/svg+xml" href="/images/logo.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>designer</title>
8
- <script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-XZWBSnmF.js"></script>
9
- <link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-C3K_lyOl.css">
8
+ <script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-D7W5yEF4.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-B5zmn1Wt.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -25,7 +25,7 @@ amis_schema: |-
25
25
  "isMobile": "${window:innerWidth <= 768}"
26
26
  },
27
27
  "schemaApi": {
28
- "url": "${isMobile ? '/api/v6/functions/pages/schema?pageId=flow_selector_mobile' : '/api/v6/functions/pages/schema?pageId=flow_selector'}",
28
+ "url": "${isMobile ? '/api/workflow/pages/schema?pageId=flow_selector_mobile' : '/api/workflow/pages/schema?pageId=flow_selector'}",
29
29
  "method": "get"
30
30
  },
31
31
  "initFetchSchema": true,
@@ -25,7 +25,7 @@ amis_schema: |-
25
25
  "isMobile": "${window:innerWidth <= 768}"
26
26
  },
27
27
  "schemaApi": {
28
- "url": "${isMobile ? '/api/v6/functions/pages/schema?pageId=flow_selector_mobile' : '/api/v6/functions/pages/schema?pageId=flow_selector'}",
28
+ "url": "${isMobile ? '/api/workflow/pages/schema?pageId=flow_selector_mobile' : '/api/workflow/pages/schema?pageId=flow_selector'}",
29
29
  "method": "get"
30
30
  },
31
31
  "initFetchSchema": true,
@@ -38,6 +38,11 @@ fields:
38
38
  label: Flow Snapshot
39
39
  name: flow_snapshot
40
40
  is_wide: true
41
+ object_workflows_snapshot:
42
+ type: textarea
43
+ label: Object Workflows Snapshot
44
+ name: object_workflows_snapshot
45
+ is_wide: true
41
46
  list_views:
42
47
  all:
43
48
  label: All
@@ -209,37 +209,6 @@
209
209
  "id": "u:c072a969328c",
210
210
  "unmountOnExit": true
211
211
  },
212
- {
213
- "title": "${'flows_tabs_step'|t}",
214
- "body": [
215
- {
216
- "type": "steedos-object-form",
217
- "label": "对象表单",
218
- "objectApiName": "flows",
219
- "recordId": "${recordId}",
220
- "className": "bg-white",
221
- "id": "u:3b769ba2695e",
222
- "mode": "read",
223
- "fields": [
224
- "current",
225
- "current.steps",
226
- "current.steps.$.name",
227
- "current.steps.$.disableCC",
228
- "current.steps.$.allowDistribute",
229
- "current.steps.$.can_edit_main_attach",
230
- "current.steps.$.can_edit_normal_attach",
231
- "current.steps.$.cc_must_finished",
232
- "current.steps.$.cc_alert",
233
- "current.steps.$.allowBatch",
234
- "current.steps.$.oneClickApproval",
235
- "current.steps.$.oneClickRejection"
236
- ],
237
- "fieldsExtend": "{\n \"current\": {\n \"group\": null,\n \"label\": false\n },\n \n \"current.steps\": {\n \"label\": false\n }\n}"
238
- }
239
- ],
240
- "id": "u:9acfaaba0412",
241
- "unmountOnExit": true
242
- },
243
212
  {
244
213
  "title": "${'flows_tabs_field'|t}",
245
214
  "body": [
@@ -528,8 +528,7 @@ router.put('/am/flows', async function (req, res) {
528
528
  if (insCount > 0) {
529
529
  pass = true;
530
530
  }
531
-
532
- if (pass === true && flow.current.start_date && stepsStr === flowComeStepsStr) {
531
+ if (pass === true && flow.current.start_date && stepsStr != flowComeStepsStr) {
533
532
  updateObj.$push = {
534
533
  'historys': flow.current
535
534
  };
@@ -62,7 +62,7 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
62
62
  const baseURL = process.env.WORKFLOW_AI_BASE_URL || 'https://api.openai.com/v1';
63
63
  const model = process.env.WORKFLOW_AI_MODEL || 'gpt-4o';
64
64
 
65
- const { fields, events, userRequest, images, instance_template, form_script, flow_events } = req.body;
65
+ const { fields, events, userRequest, images, instance_template, form_script, flow_events, name_forumla } = req.body;
66
66
 
67
67
  if (!userRequest) {
68
68
  sendEvent('error', { error: '请输入需求描述' });
@@ -89,6 +89,9 @@ router.post('/am/ai/form-design-stream', async function auth(req, res, next) {
89
89
  if (flow_events) {
90
90
  extraSections += `\n\n## 流程事件 (flow_events)\n\`\`\`javascript\n${flow_events}\n\`\`\``;
91
91
  }
92
+ if (name_forumla) {
93
+ extraSections += `\n\n## 旧版标题公式 (name_forumla)\n${name_forumla}\n\n说明:标题公式用 {字段名} 引用字段值,升级时需将其中的 {旧字段名} 替换为 {新版字段name},输出到 nameFormula 字段。`;
94
+ }
92
95
 
93
96
  const userContent = `## 当前表单字段\n${fieldsJson}\n\n## 当前事件脚本\n${eventsJson}${extraSections}\n\n## 用户需求\n${userRequest}\n\n请返回修改后的完整 JSON 对象(包含 tableColumns、fields 和 events)。`;
94
97
 
@@ -438,6 +441,7 @@ function buildSystemPrompt() {
438
441
  - "orgMulti" — 组织(多选,选择多个部门)
439
442
  - "reference" — 对象选择(选择指定对象的记录,支持多选,可展示记录的多个字段)
440
443
  - "richtext" — 富文本(支持格式化文本、颜色、链接等)
444
+ - "autoNumber" — 自动编号(只读字段,自动生成带前缀、日期、序号的唯一编号)
441
445
  - required: 布尔值,是否必填
442
446
  - colspan: 数字,字段占几列,取值范围 1~tableColumns。默认 1(占一列),设为 tableColumns 则独占一行。section 和 table 始终等于 tableColumns
443
447
  - description: 字符串,字段说明/提示文字,支持 HTML
@@ -551,6 +555,16 @@ function buildSystemPrompt() {
551
555
  - displayFields: 数组,选择记录后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
552
556
  - pickerMultiple: 布尔值,是否允许多选,默认 true
553
557
 
558
+ - autoNumber 类型(自动编号):
559
+ - 自动编号字段始终为只读(readonly: true),运行时由系统自动生成唯一编号
560
+ - autoNumberPrefix: 字符串,编号前缀,如 "SN-"、"HT-",默认 ""
561
+ - autoNumberSuffix: 字符串,编号后缀,默认 ""
562
+ - autoNumberPadding: 数字,序号位数(补零),默认 4,如 4 → 0001
563
+ - autoNumberDateFormat: 字符串,日期片段格式,可选值:"none"(无日期) | "YYYY"(年) | "YYYYMM"(年月,默认) | "YYYYMMDD"(年月日)
564
+ - autoNumberResetCycle: 字符串,序号重置周期,可选值:"none"(不重置) | "yearly"(每年) | "monthly"(每月,默认) | "daily"(每天)
565
+ - 生成的编号格式示例:前缀 + 日期 + 序号 + 后缀,如 "SN-202603-0001"
566
+ - 示例:{ "type": "autoNumber", "label": "合同编号", "name": "contract_no", "readonly": true, "autoNumberPrefix": "HT-", "autoNumberDateFormat": "YYYYMM", "autoNumberPadding": 4, "autoNumberResetCycle": "monthly" }
567
+
554
568
  - 可见性规则(任何字段都可设置):
555
569
  - visibilityRules: 数组,控制字段显示/隐藏的规则,当所有/任一规则满足时字段可见,否则隐藏。每条规则格式:
556
570
  { "field": "控制字段的name", "operator": "操作符", "value": "比较值" }
@@ -718,6 +732,19 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
718
732
  \`\`\`json
719
733
  { "name": "apply_increase_amount", "type": "number", "max": 4999999.99, "precision": 2 }
720
734
  \`\`\`
735
+ ### 旧版自动编号字段迁移
736
+ - 旧版表单中的自动编号字段通过 default_value 属性实现,格式为 \`default_value = "auto_number('规则名称')"\`
737
+ - 新版必须将此类字段转换为 \`type: "autoNumber"\` 类型
738
+ - 迁移规则:
739
+ - 遇到字段的 default_value 包含 \`auto_number(...)\` 时,将该字段的 type 改为 "autoNumber",设置 readonly: true
740
+ - 删除原有的 default_value 属性
741
+ - 如果旧版编号规则名称中能推断出前缀、日期格式等信息,相应设置 autoNumberPrefix、autoNumberDateFormat 等属性;否则使用默认值
742
+ - 默认配置:autoNumberPrefix: ""、autoNumberPadding: 4、autoNumberDateFormat: "YYYYMM"、autoNumberResetCycle: "monthly"
743
+ - 迁移示例:
744
+ 旧版字段:{ "name": "合同编号", "type": "text", "default_value": "auto_number('contract_no')" }
745
+ 新版字段:{ "name": "contract_no", "label": "合同编号", "type": "autoNumber", "readonly": true, "autoNumberPrefix": "", "autoNumberPadding": 4, "autoNumberDateFormat": "YYYYMM", "autoNumberResetCycle": "monthly" }
746
+ - migrationLog 记录格式:"字段 <name>: default_value auto_number(...) 转换为 autoNumber 类型"
747
+
721
748
  ### 旧版 HTML 字段类型迁移
722
749
  - 旧版表单中存在 type 为 \"html\" 的字段,这是旧版的 HTML 编辑器字段,允许用户输入和编辑 HTML 格式的富文本内容
723
750
  - 新版表单中 **不再支持 \"html\" 类型**,已被 **\"richtext\"(富文本)** 类型完全替代
@@ -754,9 +781,17 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
754
781
  "onValueChange": "脚本代码或空字符串",
755
782
  "onSubmit": "脚本代码或空字符串"
756
783
  },
784
+ "nameFormula": "标题公式(仅升级场景,将旧版标题公式中的字段引用更新为新版字段 name)",
757
785
  "migrationLog": ["迁移记录,仅升级场景需要"]
758
786
  }
759
787
 
788
+ ### nameFormula(标题公式,升级场景)
789
+ 当用户提供了旧版标题公式(name_forumla)时,需要将其中的字段引用更新为新版字段 name。
790
+ - 旧版标题公式格式:用 {字段名} 引用字段值,如 "{申请人} 的 {类型} 申请"
791
+ - 升级时将 {旧字段名} 替换为 {新字段name},如 "{applicant_name} 的 {request_type} 申请"
792
+ - 公式中的纯文本部分保持不变
793
+ - 如果用户未提供旧版标题公式,则 nameFormula 字段可省略或返回空字符串
794
+
760
795
  ### migrationLog(迁移记录)
761
796
  当用户提供了 flow_events、form_script、instance_template 中包含业务逻辑(如校验、赋值、显隐控制等脚本代码)时,必须在 migrationLog 数组中为每条被迁移的逻辑记录一条说明,格式:
762
797
  "<来源>: <原逻辑简述> → <迁移到的具体位置>"
@@ -65,7 +65,9 @@ const getNextSteps = async (flow, flowVersionId, instance, currentStep, judge, a
65
65
 
66
66
  let nextSteps = [];
67
67
  const lines = currentStep.lines;
68
-
68
+ if(!currentStep.id){
69
+ currentStep.id = currentStep._id;
70
+ }
69
71
  switch (currentStep.step_type) {
70
72
  case 'condition': //条件
71
73
  const stepIds = await UUFlowManager.getNextSteps(instance, flow, currentStep, judge, autoFormDoc);
@@ -336,6 +338,11 @@ router.post('/api/workflow/v2/nextSteps', requireAuthentication, async function
336
338
  const form = await objectql.getObject('forms').findOne(instance.form);
337
339
  const { fields } = await UUFlowManager.getFormVersion(form, instance.form_version);
338
340
  const resNextSteps = await calcSteps(instance, flow, flowVersionId, fields, values);
341
+
342
+ _.map(resNextSteps, (step) => {
343
+ step.stepHandler = instance.step_approve ? instance.step_approve[step._id] : null;
344
+ });
345
+
339
346
  res.status(200).send({
340
347
  'nextSteps': resNextSteps
341
348
  });
@@ -269,7 +269,10 @@ router.post('/api/workflow/v2/nextStepUsersValue', requireAuthentication, async
269
269
  for (let i = traces.length - 1; i >= 0; i--) {
270
270
  const trace = traces[i];
271
271
  if (trace.step === nextStepId && trace.approves && trace.approves.length > 0) {
272
- newNextUsers = trace.approves.map(approve => approve.handler);
272
+ // 需要排除以下 approve.jude ['returned', 'terminated', 'retrieved'].includes(judge)
273
+ newNextUsers = trace.approves
274
+ .filter(approve => !['returned', 'terminated', 'retrieved'].includes(approve.judge))
275
+ .map(approve => approve.handler);
273
276
  break;
274
277
  }
275
278
  }
@@ -55,11 +55,40 @@ router.post('/api/workflow/submit', requireAuthentication, async function (req,
55
55
  var hashData = req.body;
56
56
  var result = [];
57
57
  const instance_from_client = hashData['Instances'][0];
58
- // beforeDraftSubmit
59
58
  const insId = instance_from_client._id;
60
- await excuteTriggers({ when: 'beforeDraftSubmit', userId: userId, flowId: instance_from_client['flow'], insId: insId });
61
59
 
62
60
  try {
61
+ // 校验流程启用状态、流程版本和表单版本是否为最新
62
+ const flowsCollection = await getCollection('flows');
63
+ const formsCollection = await getCollection('forms');
64
+ const ins = await db.instances.findOne({ _id: insId });
65
+ if (!ins) {
66
+ throw new Error('申请单不存在');
67
+ }
68
+ const flow = await flowsCollection.findOne({ _id: ins.flow }, { projection: { state: 1, current: 1 } });
69
+ if (!flow) {
70
+ throw new Error('流程不存在');
71
+ }
72
+ // 校验流程是否已启用
73
+ if (flow.state !== 'enabled') {
74
+ throw new Error('流程未启用,操作失败');
75
+ }
76
+ // 校验流程版本是否为最新版
77
+ if (ins.flow_version !== flow.current._id) {
78
+ throw new Error('流程已升级,请刷新后重新填写提交');
79
+ }
80
+ const form = await formsCollection.findOne({ _id: ins.form }, { projection: { current: 1 } });
81
+ if (!form) {
82
+ throw new Error('表单不存在');
83
+ }
84
+ // 校验表单版本是否为最新版
85
+ if (ins.form_version !== form.current._id) {
86
+ throw new Error('表单已升级,请刷新后重新填写提交');
87
+ }
88
+
89
+ // beforeDraftSubmit
90
+ await excuteTriggers({ when: 'beforeDraftSubmit', userId: userId, flowId: instance_from_client['flow'], insId: insId });
91
+
63
92
  var r = await UUFlowManager.submit_instance(instance_from_client, userSession);
64
93
  if (r.alerts) {
65
94
  result.push(r);
@@ -621,7 +621,8 @@
621
621
  break;
622
622
  case "section":
623
623
  tpl.type = "steedos-field-group";
624
- tpl.title = field.name;
624
+ // 历史表单 section 字段可能只有 code 没有 name,需要 fallback 到 code,避免 title undefined 导致保存时分组字段丢失
625
+ tpl.title = field.name || field.code;
625
626
  tpl.description = field.description;
626
627
  tpl.body = []
627
628
  if (field.fields) {
@@ -80,7 +80,7 @@ router.get('/api/object_workflows/workflow_field/options', requireAuthentication
80
80
  if (f.fields) {
81
81
  f.fields.forEach(function (ff) {
82
82
  form_fields.push({
83
- 'label': ff.name || ff.code,
83
+ 'label': ff.label || ff.name || ff.code,
84
84
  'value': ff.code
85
85
  });
86
86
  });
@@ -89,14 +89,14 @@ router.get('/api/object_workflows/workflow_field/options', requireAuthentication
89
89
  if (f.fields) {
90
90
  f.fields.forEach(function (ff) {
91
91
  form_fields.push({
92
- 'label': (f.name || f.code) + "=>" + (ff.name || ff.code),
92
+ 'label': (f.label || f.name || f.code) + "=>" + (ff.label || ff.name || ff.code),
93
93
  'value': f.code + '.' + ff.code
94
94
  });
95
95
  });
96
96
  }
97
97
  } else {
98
98
  form_fields.push({
99
- 'label': f.name || f.code,
99
+ 'label': f.label || f.name || f.code,
100
100
  'value': f.code
101
101
  });
102
102
  }
@@ -0,0 +1,220 @@
1
+ const assert = require('assert');
2
+ const mod = require('../../../src/rests/migrateApprovalCommentsField');
3
+
4
+ const { matchesApprovalPattern, transformField, processFieldsArray } = mod._helpers;
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // matchesApprovalPattern tests
8
+ // ---------------------------------------------------------------------------
9
+ const patternCases = [
10
+ { input: '{traces.步骤名}', expected: true },
11
+ { input: '${traces.步骤名}', expected: true },
12
+ { input: '{signature.traces.步骤名}', expected: true },
13
+ { input: '${signature.traces.步骤名}', expected: true },
14
+ { input: '{yijianlan:{step:"步骤名"}}', expected: true },
15
+ { input: '{普通字段}', expected: false },
16
+ { input: null, expected: false },
17
+ { input: undefined, expected: false }
18
+ ];
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // processFieldsArray: flat (first-level) fields
22
+ // ---------------------------------------------------------------------------
23
+ const flatFields = [
24
+ {
25
+ type: 'input',
26
+ code: '主管经理意见',
27
+ formula: '{signature.traces.主管经理审核}',
28
+ _id: 'id-1'
29
+ },
30
+ {
31
+ type: 'input',
32
+ code: '普通字段',
33
+ _id: 'id-2'
34
+ }
35
+ ];
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // processFieldsArray: nested section fields (the bug scenario)
39
+ // ---------------------------------------------------------------------------
40
+ const nestedFields = [
41
+ {
42
+ type: 'section',
43
+ code: '申请单位意见',
44
+ _id: 'sec-1',
45
+ fields: [
46
+ {
47
+ type: 'input',
48
+ code: '部门负责人意见',
49
+ formula: '{signature.traces.申请单位部门负责人意见}',
50
+ _id: 'id-3'
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ type: 'section',
56
+ code: '财务处意见',
57
+ _id: 'sec-2',
58
+ fields: [
59
+ {
60
+ type: 'input',
61
+ code: '部门负责人意见1',
62
+ name: '部门负责人意见',
63
+ formula: '{signature.traces.财务处部门负责人意见}',
64
+ _id: 'id-4'
65
+ }
66
+ ]
67
+ }
68
+ ];
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // processFieldsArray: deeply nested (section > section)
72
+ // ---------------------------------------------------------------------------
73
+ const deeplyNestedFields = [
74
+ {
75
+ type: 'section',
76
+ code: '外层',
77
+ _id: 'sec-outer',
78
+ fields: [
79
+ {
80
+ type: 'panel',
81
+ code: '内层',
82
+ _id: 'panel-inner',
83
+ fields: [
84
+ {
85
+ type: 'input',
86
+ code: '深层签字字段',
87
+ formula: '{traces.深层步骤}',
88
+ _id: 'id-deep'
89
+ }
90
+ ]
91
+ }
92
+ ]
93
+ }
94
+ ];
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // processFieldsArray: mixed top-level and nested
98
+ // ---------------------------------------------------------------------------
99
+ const mixedFields = [
100
+ {
101
+ type: 'input',
102
+ code: '顶层签字字段',
103
+ formula: '{signature.traces.顶层步骤}',
104
+ _id: 'id-top'
105
+ },
106
+ {
107
+ type: 'section',
108
+ code: '分组',
109
+ _id: 'sec-mixed',
110
+ fields: [
111
+ {
112
+ type: 'input',
113
+ code: '嵌套签字字段',
114
+ formula: '{signature.traces.嵌套步骤}',
115
+ _id: 'id-nested'
116
+ },
117
+ {
118
+ type: 'input',
119
+ code: '普通嵌套字段',
120
+ _id: 'id-plain-nested'
121
+ }
122
+ ]
123
+ }
124
+ ];
125
+
126
+ const run = () => {
127
+ console.log('[migrateApprovalCommentsField] running tests...');
128
+
129
+ // --- matchesApprovalPattern ---
130
+ for (const item of patternCases) {
131
+ assert.strictEqual(
132
+ matchesApprovalPattern(item.input),
133
+ item.expected,
134
+ `matchesApprovalPattern("${item.input}") should be ${item.expected}`
135
+ );
136
+ }
137
+ console.log(` matchesApprovalPattern: ${patternCases.length} assertions passed.`);
138
+
139
+ // --- transformField: should transform a matching field ---
140
+ const transformed = transformField(flatFields[0]);
141
+ assert.ok(transformed, 'transformField should return a non-null result for matching field');
142
+ assert.strictEqual(transformed.type, 'steedos-field');
143
+ assert.strictEqual(transformed.__approval_comments_transformed, true);
144
+ assert.strictEqual(transformed.config.steps[0].name, '主管经理审核');
145
+ assert.strictEqual(transformed.config.steps[0].show_image_sign, true);
146
+
147
+ // --- transformField: should skip non-matching field ---
148
+ const skipped = transformField(flatFields[1]);
149
+ assert.strictEqual(skipped, null, 'transformField should return null for non-matching field');
150
+
151
+ // --- transformField: should skip already-transformed field ---
152
+ const alreadyTransformed = { ...flatFields[0], __approval_comments_transformed: true };
153
+ assert.strictEqual(transformField(alreadyTransformed), null, 'transformField should skip already-transformed fields');
154
+ console.log(' transformField: 4 assertions passed.');
155
+
156
+ // --- processFieldsArray: flat fields ---
157
+ const flatResult = processFieldsArray(flatFields, 'current.fields');
158
+ assert.strictEqual(flatResult.updates.length, 1, 'Flat: should transform 1 matching field');
159
+ assert.strictEqual(flatResult.updates[0].path, 'current.fields.0', 'Flat: path should be current.fields.0');
160
+ assert.ok(flatResult.unsets.includes('current.fields.0.formula'), 'Flat: formula should be in unsets');
161
+ console.log(' processFieldsArray (flat): 3 assertions passed.');
162
+
163
+ // --- processFieldsArray: nested section fields (the bug regression test) ---
164
+ const nestedResult = processFieldsArray(nestedFields, 'current.fields');
165
+ assert.strictEqual(nestedResult.updates.length, 2,
166
+ 'Nested: should transform 2 matching fields inside sections');
167
+ const nestedPaths = nestedResult.updates.map(u => u.path);
168
+ assert.ok(nestedPaths.includes('current.fields.0.fields.0'),
169
+ 'Nested: first nested field path should be current.fields.0.fields.0');
170
+ assert.ok(nestedPaths.includes('current.fields.1.fields.0'),
171
+ 'Nested: second nested field path should be current.fields.1.fields.0');
172
+ assert.ok(nestedResult.unsets.includes('current.fields.0.fields.0.formula'),
173
+ 'Nested: formula unset path should be current.fields.0.fields.0.formula');
174
+ assert.ok(nestedResult.unsets.includes('current.fields.1.fields.0.formula'),
175
+ 'Nested: formula unset path should be current.fields.1.fields.0.formula');
176
+
177
+ // Verify step names are correct
178
+ const step0 = nestedResult.updates.find(u => u.path === 'current.fields.0.fields.0');
179
+ assert.strictEqual(step0.update.config.steps[0].name, '申请单位部门负责人意见',
180
+ 'Nested: step name should match formula');
181
+ const step1 = nestedResult.updates.find(u => u.path === 'current.fields.1.fields.0');
182
+ assert.strictEqual(step1.update.config.steps[0].name, '财务处部门负责人意见',
183
+ 'Nested: step name should match formula');
184
+ console.log(' processFieldsArray (nested sections): 7 assertions passed.');
185
+
186
+ // --- processFieldsArray: deeply nested (section > panel) ---
187
+ const deepResult = processFieldsArray(deeplyNestedFields, 'current.fields');
188
+ assert.strictEqual(deepResult.updates.length, 1, 'Deep: should transform 1 deeply nested field');
189
+ assert.strictEqual(deepResult.updates[0].path, 'current.fields.0.fields.0.fields.0',
190
+ 'Deep: path should reflect full nesting depth');
191
+ assert.ok(deepResult.unsets.includes('current.fields.0.fields.0.fields.0.formula'),
192
+ 'Deep: formula unset should use full nested path');
193
+ console.log(' processFieldsArray (deeply nested): 3 assertions passed.');
194
+
195
+ // --- processFieldsArray: mixed top-level and nested ---
196
+ const mixedResult = processFieldsArray(mixedFields, 'current.fields');
197
+ assert.strictEqual(mixedResult.updates.length, 2, 'Mixed: should transform 2 fields (1 top + 1 nested)');
198
+ const mixedPaths = mixedResult.updates.map(u => u.path);
199
+ assert.ok(mixedPaths.includes('current.fields.0'), 'Mixed: top-level field path should be current.fields.0');
200
+ assert.ok(mixedPaths.includes('current.fields.1.fields.0'), 'Mixed: nested field path should be current.fields.1.fields.0');
201
+ console.log(' processFieldsArray (mixed): 3 assertions passed.');
202
+
203
+ // --- processFieldsArray: historys path prefix ---
204
+ const historyResult = processFieldsArray(nestedFields, 'historys.0.fields');
205
+ assert.strictEqual(historyResult.updates.length, 2, 'History: should transform 2 nested fields');
206
+ const historyPaths = historyResult.updates.map(u => u.path);
207
+ assert.ok(historyPaths.includes('historys.0.fields.0.fields.0'),
208
+ 'History: nested field path should use historys prefix');
209
+ assert.ok(historyPaths.includes('historys.0.fields.1.fields.0'),
210
+ 'History: nested field path should use historys prefix');
211
+ console.log(' processFieldsArray (historys prefix): 3 assertions passed.');
212
+
213
+ console.log('[migrateApprovalCommentsField] All assertions passed.');
214
+ };
215
+
216
+ if (require.main === module) {
217
+ run();
218
+ }
219
+
220
+ module.exports = { run };