@steedos-labs/plugin-workflow 3.0.88 → 3.0.90
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/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/{index-Cg85h361.js → index-jGaJitaP.js} +75 -75
- package/designer/dist/index.html +1 -1
- package/main/default/client/flow2_render.client.js +1 -1
- package/main/default/manager/import.js +2 -1
- package/main/default/manager/uuflow_manager.js +23 -3
- package/main/default/routes/api_designer_startup.router.js +2 -1
- package/main/default/test/test_handleSkipProcessed_handlerError.js +142 -0
- package/main/default/utils/designerManager.js +3 -2
- package/package.json +3 -2
- package/public/amis-renderer/amis-renderer.js +1 -1
package/designer/dist/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
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-
|
|
8
|
+
<script type="module" crossorigin src="/api/workflow/designer-v2/assets/index-jGaJitaP.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/api/workflow/designer-v2/assets/index-CNcDjHgd.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -151,7 +151,8 @@ async function upgradeForm(formId, form, currentUserId, spaceId) {
|
|
|
151
151
|
'help_text': form["help_text"],
|
|
152
152
|
'error_message': form["error_message"],
|
|
153
153
|
'category': form["category"],
|
|
154
|
-
'instance_style': form["instance_style"]
|
|
154
|
+
'instance_style': form["instance_style"],
|
|
155
|
+
'chineseFieldNames': form["chineseFieldNames"] || false
|
|
155
156
|
};
|
|
156
157
|
|
|
157
158
|
return formCollection.updateOne({ _id: formId }, formUpdateObj);
|
|
@@ -1722,7 +1722,12 @@ UUFlowManager.workflow_engine = async function (approve_from_client, current_use
|
|
|
1722
1722
|
}
|
|
1723
1723
|
}
|
|
1724
1724
|
// Skip Processed: Check if next step should be auto-skipped
|
|
1725
|
-
|
|
1725
|
+
// 滑步是已审批之后的附加处理,不应让其异常导致审批主流程失败回滚。
|
|
1726
|
+
try {
|
|
1727
|
+
await UUFlowManager.handleSkipProcessed(instance_id, flow);
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
console.error('[workflow/engine] [workflow_engine] handleSkipProcessed failed, ignored', error && error.message);
|
|
1730
|
+
}
|
|
1726
1731
|
console.log(`[workflow/engine] [workflow_engine] handleSkipProcessed took ${Date.now() - t8_skip}ms`);
|
|
1727
1732
|
|
|
1728
1733
|
const t9 = Date.now();
|
|
@@ -2002,7 +2007,15 @@ UUFlowManager.handleSkipProcessed = async function (instance_id, flow, maxDepth
|
|
|
2002
2007
|
} else {
|
|
2003
2008
|
// Create new trace for the next step
|
|
2004
2009
|
const space_id = instance.space;
|
|
2005
|
-
|
|
2010
|
+
let next_step_users;
|
|
2011
|
+
try {
|
|
2012
|
+
next_step_users = await HandlersManager.getHandlers(instance_id, next_step_id, instance.applicant);
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
// 下一步处理人解析失败(如角色/岗位未匹配到人)时,中止滑步,
|
|
2015
|
+
// 让实例停留在当前已提交的步骤,避免把异常抛回已提交成功的主流程。
|
|
2016
|
+
console.warn('[workflow/engine] [handleSkipProcessed] getHandlers failed, aborting skip', error && error.message);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2006
2019
|
|
|
2007
2020
|
if (!next_step_users || next_step_users.length === 0) {
|
|
2008
2021
|
console.warn('[workflow/engine] [handleSkipProcessed] No handlers found for next step after skip');
|
|
@@ -3825,8 +3838,15 @@ UUFlowManager.submit_instance = async function (instance_from_client, user_info)
|
|
|
3825
3838
|
);
|
|
3826
3839
|
|
|
3827
3840
|
// Skip Processed: Check if next step should be auto-skipped
|
|
3841
|
+
// 申请单已提交(state 已更新为 pending),滑步是提交后的附加处理。
|
|
3842
|
+
// 滑步失败不能把异常抛回提交接口,否则前台会提示失败但申请单其实已提交成功。
|
|
3843
|
+
// 失败时让实例停留在第一个审批步骤。
|
|
3828
3844
|
if (next_step.step_type !== "end") {
|
|
3829
|
-
|
|
3845
|
+
try {
|
|
3846
|
+
await UUFlowManager.handleSkipProcessed(instance_id, flow);
|
|
3847
|
+
} catch (error) {
|
|
3848
|
+
console.error('[workflow/engine] [submit_instance] handleSkipProcessed failed, ignored', error && error.message);
|
|
3849
|
+
}
|
|
3830
3850
|
}
|
|
3831
3851
|
|
|
3832
3852
|
if (next_step.step_type !== "end") {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回归测试:滑步(handleSkipProcessed)解析下一步处理人抛异常时,
|
|
3
|
+
* 不能把异常抛回已提交/已审批的主流程。
|
|
4
|
+
*
|
|
5
|
+
* 背景(线上现象):
|
|
6
|
+
* - 接口 /api/workflow/submit 返回
|
|
7
|
+
* {"errors":[{"errorMessage":"error! 根据user_id和role_ids没查到对应的处理人"}]}
|
|
8
|
+
* 前台弹出该错误,但申请单其实已经提交出去了。
|
|
9
|
+
* - 根因:submit_instance 先把实例 state 改成 pending(已提交,COMMIT),
|
|
10
|
+
* 然后才调用 handleSkipProcessed(提交后的附加处理)。当第一个审批步骤的
|
|
11
|
+
* 处理人就是申请人时,会触发滑步去解析“下一步骤”的处理人;若下一步用了
|
|
12
|
+
* 角色/岗位类处理人且没匹配到人,HandlersManager.getHandlers 会 throw。
|
|
13
|
+
* 该异常一路冒泡到路由 catch,返回 errors,但实例早已提交、无法回滚 →
|
|
14
|
+
* “前台报错但申请单已提交”。
|
|
15
|
+
*
|
|
16
|
+
* 期望(修复后):
|
|
17
|
+
* - getHandlers 抛异常时,滑步中止(不再向上抛),handleSkipProcessed 正常返回。
|
|
18
|
+
* - 实例不会被滑步进一步改动,停留在当前(第一个)审批步骤。
|
|
19
|
+
*
|
|
20
|
+
* 用法:
|
|
21
|
+
* node main/default/test/test_handleSkipProcessed_handlerError.js
|
|
22
|
+
* 或 npm run test:skip-handler-error
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const assert = require('assert');
|
|
26
|
+
|
|
27
|
+
// uuflow_manager 依赖运行时全局 `_`(lodash,由 Steedos server 注入),
|
|
28
|
+
// 独立跑测试时手动补上。
|
|
29
|
+
global._ = global._ || require('lodash');
|
|
30
|
+
|
|
31
|
+
// 注入假 getCollection,使 uuflow_manager 不依赖真实数据库。
|
|
32
|
+
// uuflow_manager 在加载时解构 `const { getCollection } = require('../utils/collection')`,
|
|
33
|
+
// 解构后无法从外部改写其绑定;因此在 require uuflow_manager 之前,先把
|
|
34
|
+
// utils/collection.js 的 require 缓存替换成假模块(两处解析到同一绝对路径,缓存键相同)。
|
|
35
|
+
let updateCallCount = 0;
|
|
36
|
+
const dummyCollection = {
|
|
37
|
+
// 修复正确时,else 分支在 getHandlers 抛错前不会用到这些方法;
|
|
38
|
+
// 一旦被调用说明滑步越过了中止点,计数用于断言“实例未被改动”。
|
|
39
|
+
updateOne: async function () { updateCallCount++; return { modifiedCount: 1 }; },
|
|
40
|
+
findOne: async function () { return null; },
|
|
41
|
+
find: function () { return { toArray: async () => [] }; },
|
|
42
|
+
};
|
|
43
|
+
const collectionPath = require.resolve('../utils/collection.js');
|
|
44
|
+
require.cache[collectionPath] = {
|
|
45
|
+
id: collectionPath,
|
|
46
|
+
filename: collectionPath,
|
|
47
|
+
loaded: true,
|
|
48
|
+
exports: {
|
|
49
|
+
getCollection: async () => dummyCollection,
|
|
50
|
+
_makeNewID: () => 'fake_id',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const UUFlowManager = require('../manager/uuflow_manager.js');
|
|
55
|
+
const HandlersManager = require('../manager/handlers_manager.js');
|
|
56
|
+
|
|
57
|
+
let passed = 0;
|
|
58
|
+
let failed = 0;
|
|
59
|
+
|
|
60
|
+
async function run(name, fn) {
|
|
61
|
+
try {
|
|
62
|
+
await fn();
|
|
63
|
+
console.log(` ✓ ${name}`);
|
|
64
|
+
passed++;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log(` ✗ ${name}`);
|
|
67
|
+
console.log(` ${e.message}`);
|
|
68
|
+
failed++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const APPLICANT = 'user_applicant';
|
|
73
|
+
|
|
74
|
+
function buildFakeInstance() {
|
|
75
|
+
return {
|
|
76
|
+
_id: 'ins1',
|
|
77
|
+
state: 'pending',
|
|
78
|
+
space: 'space1',
|
|
79
|
+
applicant: APPLICANT,
|
|
80
|
+
processed_user_ids: [APPLICANT], // 申请人默认已处理
|
|
81
|
+
traces: [
|
|
82
|
+
{
|
|
83
|
+
_id: 'trace1',
|
|
84
|
+
step: 'step1',
|
|
85
|
+
is_finished: false,
|
|
86
|
+
previous_trace_ids: [],
|
|
87
|
+
approves: [
|
|
88
|
+
// type 缺省 → isNormalApprove 为 true;处理人就是申请人
|
|
89
|
+
{ _id: 'app1', user: APPLICANT, handler: APPLICANT },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
(async () => {
|
|
97
|
+
console.log('UUFlowManager.handleSkipProcessed 下一步处理人解析异常兜底');
|
|
98
|
+
|
|
99
|
+
await run('getHandlers 抛异常 → 滑步中止,不向上抛,实例停留在当前步骤', async () => {
|
|
100
|
+
updateCallCount = 0;
|
|
101
|
+
|
|
102
|
+
const fakeInstance = buildFakeInstance();
|
|
103
|
+
UUFlowManager.getInstance = async function (id) {
|
|
104
|
+
assert.strictEqual(id, 'ins1');
|
|
105
|
+
return fakeInstance;
|
|
106
|
+
};
|
|
107
|
+
// 第一个审批步骤(sign,处理人=申请人,可滑步),出线指向 step2
|
|
108
|
+
// 下一步 step2 仍是审批步骤(非 end / 非 condition)→ 进入解析处理人的 else 分支
|
|
109
|
+
UUFlowManager.getStep = function (instance, flow, stepId) {
|
|
110
|
+
if (stepId === 'step1') {
|
|
111
|
+
return {
|
|
112
|
+
_id: 'step1', name: 'Step1', step_type: 'sign',
|
|
113
|
+
skip_enabled: true,
|
|
114
|
+
lines: [{ state: 'approved', to_step: 'step2' }],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (stepId === 'step2') {
|
|
118
|
+
return { _id: 'step2', name: 'Step2', step_type: 'sign' };
|
|
119
|
+
}
|
|
120
|
+
throw new Error('unexpected step ' + stepId);
|
|
121
|
+
};
|
|
122
|
+
// 模拟线上根因:角色/岗位没匹配到人 → 抛错
|
|
123
|
+
HandlersManager.getHandlers = async function () {
|
|
124
|
+
throw new Error('error! 根据user_id和role_ids没查到对应的处理人');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
let thrown = null;
|
|
128
|
+
try {
|
|
129
|
+
await UUFlowManager.handleSkipProcessed('ins1', { _id: 'flow1' });
|
|
130
|
+
} catch (e) {
|
|
131
|
+
thrown = e;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
assert.strictEqual(thrown, null,
|
|
135
|
+
'修复后 getHandlers 抛错不应再向上抛,否则已提交的申请单会被路由当成失败返回');
|
|
136
|
+
assert.strictEqual(updateCallCount, 0,
|
|
137
|
+
'滑步中止后不应再改动实例,申请单应停留在第一个审批步骤');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
141
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
142
|
+
})();
|
|
@@ -70,13 +70,13 @@ async function getByAdminSpaceIds(spaceIds, companyId, isCompanyAdmin, flowId) {
|
|
|
70
70
|
}
|
|
71
71
|
return await getObject('forms').find({
|
|
72
72
|
filters: formsFilters,
|
|
73
|
-
fields: ['name', 'state', 'is_deleted', 'is_valid', 'space', 'description', 'help_text', 'created', 'created_by', 'error_message', 'current', 'enable_workflow', 'enable_view_others', 'app', 'category', 'is_subform', 'instance_style', 'company_id']
|
|
73
|
+
fields: ['name', 'state', 'is_deleted', 'is_valid', 'space', 'description', 'help_text', 'created', 'created_by', 'error_message', 'current', 'enable_workflow', 'enable_view_others', 'app', 'category', 'is_subform', 'instance_style', 'company_id', 'chineseFieldNames']
|
|
74
74
|
})
|
|
75
75
|
})
|
|
76
76
|
} else {
|
|
77
77
|
formsPromise = getObject('forms').find({
|
|
78
78
|
filters: filters,
|
|
79
|
-
fields: ['name', 'state', 'is_deleted', 'is_valid', 'space', 'description', 'help_text', 'created', 'created_by', 'error_message', 'current', 'enable_workflow', 'enable_view_others', 'app', 'category', 'is_subform', 'instance_style', 'company_id']
|
|
79
|
+
fields: ['name', 'state', 'is_deleted', 'is_valid', 'space', 'description', 'help_text', 'created', 'created_by', 'error_message', 'current', 'enable_workflow', 'enable_view_others', 'app', 'category', 'is_subform', 'instance_style', 'company_id', 'chineseFieldNames']
|
|
80
80
|
})
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -223,6 +223,7 @@ async function updateForm(formId, form, forms, flows, currentUserId) {
|
|
|
223
223
|
'created': now,
|
|
224
224
|
'created_by': currentUserId,
|
|
225
225
|
'steps': flow.current.steps,
|
|
226
|
+
'nextEvents': flow.current.nextEvents || {},
|
|
226
227
|
'_rev': flow.current._rev + 1,
|
|
227
228
|
'flow': flow._id,
|
|
228
229
|
'form_version': current._id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steedos-labs/plugin-workflow",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.90",
|
|
4
4
|
"main": "package.service.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"export-templates": "node run.js export",
|
|
25
25
|
"convert-templates": "node convert-templates.js",
|
|
26
26
|
"test:formula-compat": "node main/default/test/test_formula_compat.js",
|
|
27
|
-
"test:approve-values": "node main/default/test/test_getApproveValues.js"
|
|
27
|
+
"test:approve-values": "node main/default/test/test_getApproveValues.js",
|
|
28
|
+
"test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"graphql-parse-resolve-info": "^4.12.3",
|