@steedos-labs/plugin-workflow 3.0.95 → 3.0.97
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/main/default/client/flow2_render.client.js +1 -1
- package/main/default/manager/import.js +4 -2
- package/main/default/manager/uuflow_manager.js +15 -0
- package/main/default/objects/flows/flows.object.yml +50 -2
- package/main/default/routes/am.router.js +22 -8
- package/main/default/test/test_ajax_error_message.js +36 -0
- package/main/default/test/test_flow_validator.js +73 -0
- package/main/default/utils/ajaxErrorMessage.js +28 -0
- package/main/default/utils/flowValidator.js +162 -0
- package/package.json +4 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const objectql = require("@steedos/objectql");
|
|
2
2
|
const _ = require('underscore');
|
|
3
3
|
const { _makeNewID, getCollection } = require('../utils/collection');
|
|
4
|
+
const { validateFlowSteps } = require('../utils/flowValidator');
|
|
4
5
|
|
|
5
6
|
const steedosImport = {};
|
|
6
7
|
|
|
@@ -244,11 +245,12 @@ async function upgradeFlow(flowCome, userId, flowId) {
|
|
|
244
245
|
updateObj.$set.name = flowCome['name'];
|
|
245
246
|
updateObj.$set.name_formula = '';
|
|
246
247
|
updateObj.$set.code_formula = '';
|
|
247
|
-
|
|
248
|
+
const flowValidation = validateFlowSteps(flowCome['current']['steps']);
|
|
249
|
+
updateObj.$set.is_valid = flowValidation.isValid;
|
|
248
250
|
updateObj.$set.flowtype = flowCome['flowtype'];
|
|
249
251
|
updateObj.$set.help_text = flowCome['help_text'];
|
|
250
252
|
updateObj.$set.decription = flowCome['descriptions'];
|
|
251
|
-
updateObj.$set.error_message =
|
|
253
|
+
updateObj.$set.error_message = flowValidation.errorMessage;
|
|
252
254
|
updateObj.$set.modified = now;
|
|
253
255
|
updateObj.$set.modified_by = userId;
|
|
254
256
|
updateObj.$set.events = flowCome['events'] || '';
|
|
@@ -1676,8 +1676,23 @@ UUFlowManager.workflow_engine = async function (approve_from_client, current_use
|
|
|
1676
1676
|
const t8 = Date.now();
|
|
1677
1677
|
// Update instance
|
|
1678
1678
|
const t8_1 = Date.now();
|
|
1679
|
+
// 当步骤发生流转时,current_step_name 的变更必须经由 objectql 写入,
|
|
1680
|
+
// 以触发 instances 的 afterUpdate(uuflowManager.triggerRecordInstanceQueue),
|
|
1681
|
+
// 否则 instance_record_queue 不会生成同步记录。
|
|
1682
|
+
// 这里先把 current_step_name 从原子 raw 写入中剥离(保留 $push traces 等原子性),
|
|
1683
|
+
// raw 写入后 DB 仍保留旧的 current_step_name,再由 objectql 单独应用新步骤名,
|
|
1684
|
+
// 使 afterUpdate 检测到 current_step_name 变化并触发同步。
|
|
1685
|
+
const nextStepNameForQueue = updateObj.$set ? updateObj.$set.current_step_name : undefined;
|
|
1686
|
+
const prevStepNameForQueue = updatedInstance.current_step_name;
|
|
1687
|
+
const stepNameChangedForQueue = nextStepNameForQueue !== undefined && nextStepNameForQueue !== prevStepNameForQueue;
|
|
1688
|
+
if (stepNameChangedForQueue) {
|
|
1689
|
+
delete updateObj.$set.current_step_name;
|
|
1690
|
+
}
|
|
1679
1691
|
const instancesCollection = await getCollection('instances');
|
|
1680
1692
|
await instancesCollection.updateOne({ _id: instance_id }, updateObj);
|
|
1693
|
+
if (stepNameChangedForQueue) {
|
|
1694
|
+
await getObject('instances').update(instance_id, { current_step_name: nextStepNameForQueue });
|
|
1695
|
+
}
|
|
1681
1696
|
console.log(`[workflow/engine] [workflow_engine] second updateOne took ${Date.now() - t8_1}ms`);
|
|
1682
1697
|
|
|
1683
1698
|
// Insert new instance tasks if needed
|
|
@@ -829,8 +829,32 @@ actions:
|
|
|
829
829
|
navigate(0)
|
|
830
830
|
},
|
|
831
831
|
error: function (XMLHttpRequest, textStatus, errorThrown) {
|
|
832
|
+
var getAjaxErrorMessage = function (xhr, textStatus, errorThrown) {
|
|
833
|
+
var responseJSON = xhr && xhr.responseJSON;
|
|
834
|
+
if (typeof responseJSON === 'string' && responseJSON) {
|
|
835
|
+
return responseJSON;
|
|
836
|
+
}
|
|
837
|
+
if (responseJSON && typeof responseJSON === 'object') {
|
|
838
|
+
if (responseJSON.message) {
|
|
839
|
+
return responseJSON.message;
|
|
840
|
+
}
|
|
841
|
+
if (responseJSON.error) {
|
|
842
|
+
return responseJSON.error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (xhr && xhr.responseText) {
|
|
846
|
+
return xhr.responseText;
|
|
847
|
+
}
|
|
848
|
+
if (errorThrown) {
|
|
849
|
+
return errorThrown;
|
|
850
|
+
}
|
|
851
|
+
if (textStatus && textStatus !== 'error') {
|
|
852
|
+
return textStatus;
|
|
853
|
+
}
|
|
854
|
+
return '操作失败';
|
|
855
|
+
};
|
|
832
856
|
SteedosUI.notification.error({
|
|
833
|
-
message: XMLHttpRequest
|
|
857
|
+
message: getAjaxErrorMessage(XMLHttpRequest, textStatus, errorThrown)
|
|
834
858
|
});
|
|
835
859
|
}
|
|
836
860
|
});
|
|
@@ -869,8 +893,32 @@ actions:
|
|
|
869
893
|
navigate(0)
|
|
870
894
|
},
|
|
871
895
|
error: function (XMLHttpRequest, textStatus, errorThrown) {
|
|
896
|
+
var getAjaxErrorMessage = function (xhr, textStatus, errorThrown) {
|
|
897
|
+
var responseJSON = xhr && xhr.responseJSON;
|
|
898
|
+
if (typeof responseJSON === 'string' && responseJSON) {
|
|
899
|
+
return responseJSON;
|
|
900
|
+
}
|
|
901
|
+
if (responseJSON && typeof responseJSON === 'object') {
|
|
902
|
+
if (responseJSON.message) {
|
|
903
|
+
return responseJSON.message;
|
|
904
|
+
}
|
|
905
|
+
if (responseJSON.error) {
|
|
906
|
+
return responseJSON.error;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (xhr && xhr.responseText) {
|
|
910
|
+
return xhr.responseText;
|
|
911
|
+
}
|
|
912
|
+
if (errorThrown) {
|
|
913
|
+
return errorThrown;
|
|
914
|
+
}
|
|
915
|
+
if (textStatus && textStatus !== 'error') {
|
|
916
|
+
return textStatus;
|
|
917
|
+
}
|
|
918
|
+
return '操作失败';
|
|
919
|
+
};
|
|
872
920
|
SteedosUI.notification.error({
|
|
873
|
-
message: XMLHttpRequest
|
|
921
|
+
message: getAjaxErrorMessage(XMLHttpRequest, textStatus, errorThrown)
|
|
874
922
|
});
|
|
875
923
|
}
|
|
876
924
|
});
|
|
@@ -5,6 +5,7 @@ const bodyParser = require('body-parser');
|
|
|
5
5
|
const steedosI18n = require("@steedos/i18n");
|
|
6
6
|
const lodash = require('lodash');
|
|
7
7
|
const { _makeNewID, getCollection } = require('../utils/collection');
|
|
8
|
+
const { validateFlowSteps } = require('../utils/flowValidator');
|
|
8
9
|
|
|
9
10
|
const designerManager = require('../utils/designerManager');
|
|
10
11
|
|
|
@@ -589,11 +590,12 @@ router.put('/am/flows', async function (req, res) {
|
|
|
589
590
|
updateObj.$set.name = flowCome['name'];
|
|
590
591
|
updateObj.$set.name_formula = '';
|
|
591
592
|
updateObj.$set.code_formula = '';
|
|
592
|
-
|
|
593
|
+
const flowValidation = validateFlowSteps(flowCome['current']['steps']);
|
|
594
|
+
updateObj.$set.is_valid = flowValidation.isValid;
|
|
593
595
|
updateObj.$set.flowtype = flowCome['flowtype'];
|
|
594
596
|
updateObj.$set.help_text = flowCome['help_text'];
|
|
595
597
|
updateObj.$set.description = flowCome['descriptions'];
|
|
596
|
-
updateObj.$set.error_message =
|
|
598
|
+
updateObj.$set.error_message = flowValidation.errorMessage;
|
|
597
599
|
updateObj.$set.modified = now;
|
|
598
600
|
updateObj.$set.modified_by = userId;
|
|
599
601
|
|
|
@@ -695,11 +697,11 @@ router.put('/am/flows/state', async function (req, res) {
|
|
|
695
697
|
if (state === 'enabled') {
|
|
696
698
|
// 流程启用前,校验其“指定历史步骤”属性中被引用的步骤是否存在且能被找到(仅限于流程的最新版)
|
|
697
699
|
let checkStepIds = [];
|
|
698
|
-
_.each(flow.current.steps, function (step) {
|
|
700
|
+
_.each(flow.current.steps || [], function (step) {
|
|
699
701
|
checkStepIds.push(step._id);
|
|
700
702
|
})
|
|
701
703
|
|
|
702
|
-
_.each(flow.current.steps, function (step) {
|
|
704
|
+
_.each(flow.current.steps || [], function (step) {
|
|
703
705
|
if (step.deal_type === 'specifyStepUser' || step.deal_type === 'specifyStepRole') {
|
|
704
706
|
if (!step.approver_step || !checkStepIds.includes(step.approver_step)) {
|
|
705
707
|
throw new Error('流程中的指定步骤不存在');
|
|
@@ -707,6 +709,19 @@ router.put('/am/flows/state', async function (req, res) {
|
|
|
707
709
|
}
|
|
708
710
|
})
|
|
709
711
|
|
|
712
|
+
const flowValidation = validateFlowSteps(flow.current.steps);
|
|
713
|
+
if (!flowValidation.isValid) {
|
|
714
|
+
await flowCollection.updateOne({_id: flowId}, {
|
|
715
|
+
$set: {
|
|
716
|
+
is_valid: false,
|
|
717
|
+
error_message: flowValidation.errorMessage,
|
|
718
|
+
modified: now,
|
|
719
|
+
modified_by: userId
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
throw new Error(flowValidation.errorMessage || '流程不合法');
|
|
723
|
+
}
|
|
724
|
+
|
|
710
725
|
// 如果 流程对应表单 是停用的 则启用
|
|
711
726
|
if (form.state === 'disabled') {
|
|
712
727
|
let formUpdateObj = {
|
|
@@ -721,9 +736,6 @@ router.put('/am/flows/state', async function (req, res) {
|
|
|
721
736
|
updatedForms.push(await formCollection.findOne({_id: formId}));
|
|
722
737
|
}
|
|
723
738
|
|
|
724
|
-
if (!flow.is_valid) {
|
|
725
|
-
throw new Error('流程不合法');
|
|
726
|
-
}
|
|
727
739
|
if (!['new', 'modify', 'delete'].includes(flow.flowtype)) {
|
|
728
740
|
throw new Error('FlowType值必须是new、modify、delete其中之一');
|
|
729
741
|
}
|
|
@@ -732,6 +744,8 @@ router.put('/am/flows/state', async function (req, res) {
|
|
|
732
744
|
}
|
|
733
745
|
|
|
734
746
|
flowUpdateObj.$set['state'] = 'enabled';
|
|
747
|
+
flowUpdateObj.$set['is_valid'] = true;
|
|
748
|
+
flowUpdateObj.$set['error_message'] = '';
|
|
735
749
|
flowUpdateObj.$set['current.modified'] = now;
|
|
736
750
|
flowUpdateObj.$set['current.start_date'] = now;
|
|
737
751
|
flowUpdateObj.$set['current.modified_by'] = userId;
|
|
@@ -1221,4 +1235,4 @@ router.post('/am/forms/addFieldsFromObject', async function (req, res) {
|
|
|
1221
1235
|
}
|
|
1222
1236
|
})
|
|
1223
1237
|
|
|
1224
|
-
exports.default = router;
|
|
1238
|
+
exports.default = router;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { getAjaxErrorMessage } = require('../utils/ajaxErrorMessage');
|
|
3
|
+
|
|
4
|
+
console.log('[ajax-error-message] running tests...');
|
|
5
|
+
|
|
6
|
+
assert.strictEqual(
|
|
7
|
+
getAjaxErrorMessage({ responseText: '流程不合法' }, 'error', 'Internal Server Error'),
|
|
8
|
+
'流程不合法'
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
assert.strictEqual(
|
|
12
|
+
getAjaxErrorMessage({ responseJSON: { message: '具体错误' }, responseText: 'fallback' }, 'error', 'Internal Server Error'),
|
|
13
|
+
'具体错误'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
assert.strictEqual(
|
|
17
|
+
getAjaxErrorMessage({ responseJSON: { error: '错误字段' } }, 'error', 'Internal Server Error'),
|
|
18
|
+
'错误字段'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
assert.strictEqual(
|
|
22
|
+
getAjaxErrorMessage({ responseJSON: '字符串错误' }, 'error', 'Internal Server Error'),
|
|
23
|
+
'字符串错误'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
assert.strictEqual(
|
|
27
|
+
getAjaxErrorMessage({}, 'error', 'Internal Server Error'),
|
|
28
|
+
'Internal Server Error'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.strictEqual(
|
|
32
|
+
getAjaxErrorMessage({}, 'error', ''),
|
|
33
|
+
'操作失败'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
console.log('[ajax-error-message] all tests passed');
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { validateFlowSteps } = require('../utils/flowValidator');
|
|
3
|
+
|
|
4
|
+
function validSteps() {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
_id: 'start',
|
|
8
|
+
name: '开始',
|
|
9
|
+
step_type: 'start',
|
|
10
|
+
lines: [{ _id: 'l1', state: 'submitted', to_step: 'approve' }]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
_id: 'approve',
|
|
14
|
+
name: '审批',
|
|
15
|
+
step_type: 'sign',
|
|
16
|
+
deal_type: 'pickupAtRuntime',
|
|
17
|
+
lines: [{ _id: 'l2', state: 'approved', to_step: 'end' }]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
_id: 'end',
|
|
21
|
+
name: '结束',
|
|
22
|
+
step_type: 'end',
|
|
23
|
+
lines: []
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assertInvalid(steps, expectedText) {
|
|
29
|
+
const result = validateFlowSteps(steps);
|
|
30
|
+
assert.strictEqual(result.isValid, false);
|
|
31
|
+
assert.ok(result.errorMessage.includes(expectedText), result.errorMessage);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log('[flow-validator] running tests...');
|
|
35
|
+
|
|
36
|
+
let result = validateFlowSteps(validSteps());
|
|
37
|
+
assert.deepStrictEqual(result, {
|
|
38
|
+
isValid: true,
|
|
39
|
+
errorMessage: '',
|
|
40
|
+
errors: []
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const noOutLine = validSteps();
|
|
44
|
+
noOutLine[1].lines = [];
|
|
45
|
+
assertInvalid(noOutLine, '审批:没有出线');
|
|
46
|
+
|
|
47
|
+
const missingTarget = validSteps();
|
|
48
|
+
missingTarget[1].lines[0].to_step = 'missing';
|
|
49
|
+
assertInvalid(missingTarget, '审批:连线目标步骤不存在');
|
|
50
|
+
|
|
51
|
+
const duplicateName = validSteps();
|
|
52
|
+
duplicateName[2].name = '审批';
|
|
53
|
+
assertInvalid(duplicateName, '步骤名称重复');
|
|
54
|
+
|
|
55
|
+
const conditionMissingExpression = validSteps();
|
|
56
|
+
conditionMissingExpression.splice(1, 0, {
|
|
57
|
+
_id: 'condition',
|
|
58
|
+
name: '条件',
|
|
59
|
+
step_type: 'condition',
|
|
60
|
+
lines: [
|
|
61
|
+
{ _id: 'c1', state: 'approved', to_step: 'approve', condition: '' },
|
|
62
|
+
{ _id: 'c2', state: 'approved', to_step: 'end', condition: 'true' }
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
conditionMissingExpression[0].lines[0].to_step = 'condition';
|
|
66
|
+
assertInvalid(conditionMissingExpression, '条件:到「审批」的出线未设置条件');
|
|
67
|
+
|
|
68
|
+
const specifyStepMissing = validSteps();
|
|
69
|
+
specifyStepMissing[1].deal_type = 'specifyStepUser';
|
|
70
|
+
specifyStepMissing[1].approver_step = 'missing';
|
|
71
|
+
assertInvalid(specifyStepMissing, '审批:指定历史步骤不存在');
|
|
72
|
+
|
|
73
|
+
console.log('[flow-validator] all tests passed');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function getAjaxErrorMessage(xhr, textStatus, errorThrown) {
|
|
2
|
+
const responseJSON = xhr && xhr.responseJSON;
|
|
3
|
+
if (typeof responseJSON === 'string' && responseJSON) {
|
|
4
|
+
return responseJSON;
|
|
5
|
+
}
|
|
6
|
+
if (responseJSON && typeof responseJSON === 'object') {
|
|
7
|
+
if (responseJSON.message) {
|
|
8
|
+
return responseJSON.message;
|
|
9
|
+
}
|
|
10
|
+
if (responseJSON.error) {
|
|
11
|
+
return responseJSON.error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (xhr && xhr.responseText) {
|
|
15
|
+
return xhr.responseText;
|
|
16
|
+
}
|
|
17
|
+
if (errorThrown) {
|
|
18
|
+
return errorThrown;
|
|
19
|
+
}
|
|
20
|
+
if (textStatus && textStatus !== 'error') {
|
|
21
|
+
return textStatus;
|
|
22
|
+
}
|
|
23
|
+
return '操作失败';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
getAjaxErrorMessage
|
|
28
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const _ = require('underscore');
|
|
2
|
+
|
|
3
|
+
const STEP_TYPES_NEED_DEAL_TYPE = ['submit', 'sign', 'counterSign', 'notify'];
|
|
4
|
+
|
|
5
|
+
function getStepId(step) {
|
|
6
|
+
return step && (step._id || step.id);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getStepName(step) {
|
|
10
|
+
const name = step && step.name;
|
|
11
|
+
return typeof name === 'string' && name.trim() ? name.trim() : getStepId(step) || '未命名步骤';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getLines(step) {
|
|
15
|
+
return Array.isArray(step && step.lines) ? step.lines : [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pushError(errors, step, field, message, extra) {
|
|
19
|
+
errors.push(Object.assign({
|
|
20
|
+
stepId: getStepId(step),
|
|
21
|
+
stepName: getStepName(step),
|
|
22
|
+
field,
|
|
23
|
+
message: `${getStepName(step)}:${message}`
|
|
24
|
+
}, extra || {}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateDealType(errors, step, stepIds) {
|
|
28
|
+
const stepType = step.step_type || 'sign';
|
|
29
|
+
const dealType = step.deal_type || '';
|
|
30
|
+
|
|
31
|
+
if (STEP_TYPES_NEED_DEAL_TYPE.includes(stepType) && !dealType) {
|
|
32
|
+
pushError(errors, step, 'deal_type', '未选择处理人身份');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (dealType === 'specifyUser' && _.isEmpty(step.approver_users)) {
|
|
37
|
+
pushError(errors, step, 'approver_users', '指定人员不能为空');
|
|
38
|
+
}
|
|
39
|
+
if (dealType === 'specifyOrg' && _.isEmpty(step.approver_orgs)) {
|
|
40
|
+
pushError(errors, step, 'approver_orgs', '指定部门不能为空');
|
|
41
|
+
}
|
|
42
|
+
if ((dealType === 'applicantRole' || dealType === 'userFieldRole' || dealType === 'orgFieldRole' || dealType === 'specifyStepRole') && _.isEmpty(step.approver_roles)) {
|
|
43
|
+
pushError(errors, step, 'approver_roles', '指定审批岗位不能为空');
|
|
44
|
+
}
|
|
45
|
+
if (dealType === 'hrRole' && _.isEmpty(step.approver_hr_roles)) {
|
|
46
|
+
pushError(errors, step, 'approver_hr_roles', '指定角色不能为空');
|
|
47
|
+
}
|
|
48
|
+
if ((dealType === 'userField' || dealType === 'userFieldRole') && !step.approver_user_field) {
|
|
49
|
+
pushError(errors, step, 'approver_user_field', '指定人员字段不能为空');
|
|
50
|
+
}
|
|
51
|
+
if ((dealType === 'orgField' || dealType === 'orgFieldRole') && !step.approver_org_field) {
|
|
52
|
+
pushError(errors, step, 'approver_org_field', '指定部门字段不能为空');
|
|
53
|
+
}
|
|
54
|
+
if ((dealType === 'specifyStepUser' || dealType === 'specifyStepRole') && (!step.approver_step || !stepIds.has(step.approver_step))) {
|
|
55
|
+
pushError(errors, step, 'approver_step', '指定历史步骤不存在');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateFlowSteps(steps) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
|
|
62
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
63
|
+
return {
|
|
64
|
+
isValid: false,
|
|
65
|
+
errorMessage: '流程的步骤不能为空',
|
|
66
|
+
errors: [{
|
|
67
|
+
field: 'steps',
|
|
68
|
+
message: '流程的步骤不能为空'
|
|
69
|
+
}]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const stepIds = new Set();
|
|
74
|
+
const nameMap = new Map();
|
|
75
|
+
const incomingMap = new Map();
|
|
76
|
+
|
|
77
|
+
steps.forEach(function (step) {
|
|
78
|
+
const stepId = getStepId(step);
|
|
79
|
+
const stepName = getStepName(step);
|
|
80
|
+
if (!stepId) {
|
|
81
|
+
pushError(errors, step, '_id', '步骤ID不能为空');
|
|
82
|
+
} else if (stepIds.has(stepId)) {
|
|
83
|
+
pushError(errors, step, '_id', '步骤ID重复');
|
|
84
|
+
} else {
|
|
85
|
+
stepIds.add(stepId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!step.name || !String(step.name).trim()) {
|
|
89
|
+
pushError(errors, step, 'name', '步骤名称不能为空');
|
|
90
|
+
}
|
|
91
|
+
nameMap.set(stepName, (nameMap.get(stepName) || 0) + 1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
steps.forEach(function (step) {
|
|
95
|
+
const stepId = getStepId(step);
|
|
96
|
+
getLines(step).forEach(function (line) {
|
|
97
|
+
if (line && line.to_step) {
|
|
98
|
+
incomingMap.set(line.to_step, (incomingMap.get(line.to_step) || 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
if (!line || !line.to_step || !stepIds.has(line.to_step)) {
|
|
101
|
+
pushError(errors, step, 'lines', '连线目标步骤不存在', { lineId: line && (line._id || line.id) });
|
|
102
|
+
}
|
|
103
|
+
if (line && line.to_step === stepId) {
|
|
104
|
+
pushError(errors, step, 'lines', '不能连接到自身', { lineId: line && (line._id || line.id) });
|
|
105
|
+
}
|
|
106
|
+
if (line && !line.state) {
|
|
107
|
+
pushError(errors, step, 'state', '连线类型不能为空', { lineId: line && (line._id || line.id) });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
nameMap.forEach(function (count, name) {
|
|
113
|
+
if (count > 1) {
|
|
114
|
+
errors.push({
|
|
115
|
+
field: 'name',
|
|
116
|
+
stepName: name,
|
|
117
|
+
message: `步骤名称重复:${name}`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
steps.forEach(function (step) {
|
|
123
|
+
const stepType = step.step_type || 'sign';
|
|
124
|
+
const lines = getLines(step);
|
|
125
|
+
const incomingCount = incomingMap.get(getStepId(step)) || 0;
|
|
126
|
+
|
|
127
|
+
if (stepType !== 'end' && lines.length === 0) {
|
|
128
|
+
pushError(errors, step, 'lines', '没有出线');
|
|
129
|
+
}
|
|
130
|
+
if (stepType === 'end' && lines.length > 0) {
|
|
131
|
+
pushError(errors, step, 'lines', '结束节点不能有出线');
|
|
132
|
+
}
|
|
133
|
+
if (stepType !== 'start' && incomingCount === 0) {
|
|
134
|
+
pushError(errors, step, 'lines', '没有入线');
|
|
135
|
+
}
|
|
136
|
+
if (stepType === 'condition') {
|
|
137
|
+
if (lines.length < 2) {
|
|
138
|
+
pushError(errors, step, 'condition_lines', '条件节点至少需要两条出线');
|
|
139
|
+
}
|
|
140
|
+
lines.forEach(function (line) {
|
|
141
|
+
if (!String((line && line.condition) || '').trim()) {
|
|
142
|
+
const target = steps.find(function (item) {
|
|
143
|
+
return getStepId(item) === line.to_step;
|
|
144
|
+
});
|
|
145
|
+
pushError(errors, step, 'condition_lines', `到「${target ? getStepName(target) : line.to_step || '未知节点'}」的出线未设置条件`, { lineId: line && (line._id || line.id) });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
validateDealType(errors, step, stepIds);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
isValid: errors.length === 0,
|
|
155
|
+
errorMessage: errors.map(function (error) { return error.message; }).join('\n'),
|
|
156
|
+
errors
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
validateFlowSteps
|
|
162
|
+
};
|
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.97",
|
|
4
4
|
"main": "package.service.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"convert-templates": "node convert-templates.js",
|
|
26
26
|
"test:formula-compat": "node main/default/test/test_formula_compat.js",
|
|
27
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
|
+
"test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js",
|
|
29
|
+
"test:flow-validator": "node main/default/test/test_flow_validator.js",
|
|
30
|
+
"test:ajax-error-message": "node main/default/test/test_ajax_error_message.js"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"graphql-parse-resolve-info": "^4.12.3",
|