@rytass/bpm-core-shared 0.21.0 → 0.21.1
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/CHANGELOG.md +43 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/lib/form.d.ts +2 -2
- package/src/lib/form.js +1 -0
- package/src/lib/form.js.map +1 -1
- package/src/lib/organization.d.ts +7 -0
- package/src/lib/organization.js +8 -0
- package/src/lib/organization.js.map +1 -1
- package/src/lib/workflow-command.d.ts +18 -1
- package/src/lib/workflow-command.js +30 -0
- package/src/lib/workflow-command.js.map +1 -1
- package/src/lib/workflow-graph.d.ts +104 -5
- package/src/lib/workflow-graph.js +321 -32
- package/src/lib/workflow-graph.js.map +1 -1
- package/src/lib/workflow-toolset.js +187 -21
- package/src/lib/workflow-toolset.js.map +1 -1
- package/src/lib/workflow.d.ts +135 -0
- package/src/lib/workflow.js +132 -0
- package/src/lib/workflow.js.map +1 -1
|
@@ -35,10 +35,15 @@ exports.toggleSelectedEdgeId = toggleSelectedEdgeId;
|
|
|
35
35
|
exports.readWorkflowDefinitionIssue = readWorkflowDefinitionIssue;
|
|
36
36
|
exports.readNotifyServiceTaskIssue = readNotifyServiceTaskIssue;
|
|
37
37
|
exports.readExcludeInitiatorIssue = readExcludeInitiatorIssue;
|
|
38
|
+
exports.readExcludeNodeActorsShapeIssue = readExcludeNodeActorsShapeIssue;
|
|
39
|
+
exports.readExcludeNodeActorsIssues = readExcludeNodeActorsIssues;
|
|
38
40
|
exports.isOrgManagerChainDecisionPolicyInvalid = isOrgManagerChainDecisionPolicyInvalid;
|
|
41
|
+
exports.readOrgManagerChainIssue = readOrgManagerChainIssue;
|
|
39
42
|
exports.readApproverResolverIssue = readApproverResolverIssue;
|
|
40
43
|
exports.readNodeActorSourceSkipReasons = readNodeActorSourceSkipReasons;
|
|
41
44
|
exports.readNodeActorSourceIssues = readNodeActorSourceIssues;
|
|
45
|
+
exports.canUserTaskHaveMultipleApprovers = canUserTaskHaveMultipleApprovers;
|
|
46
|
+
exports.readSingleApproverSourceIssues = readSingleApproverSourceIssues;
|
|
42
47
|
exports.hasConfiguredConditionEdges = hasConfiguredConditionEdges;
|
|
43
48
|
exports.readServiceTaskMemberIds = readServiceTaskMemberIds;
|
|
44
49
|
exports.defaultNotifyWebhookTargetId = defaultNotifyWebhookTargetId;
|
|
@@ -71,6 +76,7 @@ exports.readConditionExpressionOperator = readConditionExpressionOperator;
|
|
|
71
76
|
exports.readConditionExpressionValue = readConditionExpressionValue;
|
|
72
77
|
const form_1 = require("./form");
|
|
73
78
|
const workflow_1 = require("./workflow");
|
|
79
|
+
const field_access_1 = require("./field-access");
|
|
74
80
|
const workflow_reachability_1 = require("./workflow-reachability");
|
|
75
81
|
exports.EXCLUDE_INITIATOR_WHEN_EMPTY_VALUES = ['FAIL', 'INITIATOR_MANAGER', 'SKIP_NODE'];
|
|
76
82
|
exports.WORKFLOW_INPUT_HANDLE_ID = 'input';
|
|
@@ -658,16 +664,23 @@ function toggleSelectedEdgeId(edgeIds, edgeId) {
|
|
|
658
664
|
}
|
|
659
665
|
// ── Validation ─────────────────────────────────────────────────────────────
|
|
660
666
|
function readWorkflowDefinitionIssue(definition) {
|
|
661
|
-
var _a, _b;
|
|
667
|
+
var _a, _b, _c;
|
|
662
668
|
const incompleteUserTaskNode = definition.nodes.find((node) => node.type === 'userTask' &&
|
|
663
669
|
Boolean(readApproverResolverIssue(node.data.approverResolver)));
|
|
664
670
|
const unsatisfiableQuorumNode = definition.nodes.find((node) => node.type === 'userTask' &&
|
|
665
671
|
isDecisionPolicyUnsatisfiable(node.data.decisionPolicy, node.data.approverResolver));
|
|
666
672
|
const excludeInitiatorNode = definition.nodes.find((node) => node.type === 'userTask' &&
|
|
667
673
|
Boolean(readExcludeInitiatorIssue(node.data.excludeInitiator)));
|
|
668
|
-
|
|
669
|
-
|
|
674
|
+
// Shape only: whether the stages sit upstream is a publish-time graph rule
|
|
675
|
+
// (`readExcludeNodeActorsIssues`), so a draft can be saved unfinished.
|
|
676
|
+
const excludeNodeActorsIssue = (_a = definition.nodes
|
|
677
|
+
.map((node) => node.type === 'userTask'
|
|
678
|
+
? readExcludeNodeActorsShapeIssue(node.data.excludeNodeActors)
|
|
679
|
+
: null)
|
|
670
680
|
.find((issue) => issue !== null)) !== null && _a !== void 0 ? _a : null;
|
|
681
|
+
const notifyNodeIssue = (_b = definition.nodes
|
|
682
|
+
.map((node) => readNotifyServiceTaskIssue(node))
|
|
683
|
+
.find((issue) => issue !== null)) !== null && _b !== void 0 ? _b : null;
|
|
671
684
|
const incompleteConditionEdge = definition.edges.find((edge) => isExclusiveGatewaySourceEdge(edge, definition.nodes) &&
|
|
672
685
|
!edge.data.isDefault &&
|
|
673
686
|
!edge.data.condition);
|
|
@@ -690,7 +703,7 @@ function readWorkflowDefinitionIssue(definition) {
|
|
|
690
703
|
}
|
|
691
704
|
if (unsatisfiableQuorumNode &&
|
|
692
705
|
unsatisfiableQuorumNode.type === 'userTask' &&
|
|
693
|
-
((
|
|
706
|
+
((_c = unsatisfiableQuorumNode.data.decisionPolicy) === null || _c === void 0 ? void 0 : _c.type) === 'QUORUM') {
|
|
694
707
|
return (`簽核節點「${unsatisfiableQuorumNode.data.label}」的門檻人數為 ` +
|
|
695
708
|
`${unsatisfiableQuorumNode.data.decisionPolicy.threshold},` +
|
|
696
709
|
`但只指定了 ${readDesignTimeApproverCount(unsatisfiableQuorumNode.data.approverResolver)} 位簽核者,` +
|
|
@@ -699,6 +712,9 @@ function readWorkflowDefinitionIssue(definition) {
|
|
|
699
712
|
if (excludeInitiatorNode && excludeInitiatorNode.type === 'userTask') {
|
|
700
713
|
return readExcludeInitiatorIssue(excludeInitiatorNode.data.excludeInitiator);
|
|
701
714
|
}
|
|
715
|
+
if (excludeNodeActorsIssue) {
|
|
716
|
+
return excludeNodeActorsIssue;
|
|
717
|
+
}
|
|
702
718
|
if (notifyNodeIssue) {
|
|
703
719
|
return notifyNodeIssue;
|
|
704
720
|
}
|
|
@@ -750,6 +766,106 @@ function readExcludeInitiatorIssue(config) {
|
|
|
750
766
|
? null
|
|
751
767
|
: '簽核節點的「申請人不可簽核自己的單」需要指定沒有其他簽核人時的處理方式。';
|
|
752
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* The shape problem of a node's `excludeNodeActors` setting, or `null` when it
|
|
771
|
+
* is absent or well-formed: at least one id, text only, no repeats, and a
|
|
772
|
+
* known `whenEmpty`. Needs no graph, so it can block saving a draft the way
|
|
773
|
+
* {@link readExcludeInitiatorIssue} does; the graph rules are
|
|
774
|
+
* {@link readExcludeNodeActorsIssues}.
|
|
775
|
+
*/
|
|
776
|
+
function readExcludeNodeActorsShapeIssue(config) {
|
|
777
|
+
if (config === undefined) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
// Raw JSON that has not been linted yet can carry anything here.
|
|
781
|
+
const rawNodeIds = config === null || config === void 0 ? void 0 : config.nodeIds;
|
|
782
|
+
const nodeIds = Array.isArray(rawNodeIds)
|
|
783
|
+
? rawNodeIds
|
|
784
|
+
: [];
|
|
785
|
+
if (nodeIds.length === 0) {
|
|
786
|
+
return '簽核節點的「不可與前面關卡同一人」需要指定前面的簽核關卡。';
|
|
787
|
+
}
|
|
788
|
+
if (nodeIds.some((id) => typeof id !== 'string' || id.trim() === '') ||
|
|
789
|
+
new Set(nodeIds).size !== nodeIds.length) {
|
|
790
|
+
return '「不可與前面關卡同一人」所選的前面關卡不可重複,也不可留白。';
|
|
791
|
+
}
|
|
792
|
+
return exports.EXCLUDE_INITIATOR_WHEN_EMPTY_VALUES.includes(config.whenEmpty)
|
|
793
|
+
? null
|
|
794
|
+
: '簽核節點的「不可與前面關卡同一人」需要指定沒有其他簽核人時的處理方式。';
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* The designer-facing issues for a node's `excludeNodeActors` setting ("this
|
|
798
|
+
* stage's approver cannot be the person who approved an earlier stage");
|
|
799
|
+
* `[]` when it is absent or sound. `nodeId` is the node that carries the
|
|
800
|
+
* setting. The setting is checked in this order and stops at the first group
|
|
801
|
+
* that fails:
|
|
802
|
+
*
|
|
803
|
+
* 1. shape ({@link readExcludeNodeActorsShapeIssue}) and never the node itself;
|
|
804
|
+
* 2. every listed node exists and is a `userTask`;
|
|
805
|
+
* 3. each listed node on its own passes the source rules that
|
|
806
|
+
* {@link readNodeActorSourceIssues} applies to a `NODE_ACTOR` source (same
|
|
807
|
+
* judgement, wording about the exclusion):
|
|
808
|
+
* - it may not be skipped or pass without a signer
|
|
809
|
+
* ({@link readNodeActorSourceSkipReasons});
|
|
810
|
+
* - every path from the start to `nodeId` goes through it, checked for
|
|
811
|
+
* each listed node separately (never "one of them covers it");
|
|
812
|
+
* - it is upstream of `nodeId`;
|
|
813
|
+
* 4. `whenEmpty: 'SKIP_NODE'` is refused on a node whose approver fills
|
|
814
|
+
* fields in, for the same reason `excludeInitiator` is.
|
|
815
|
+
*
|
|
816
|
+
* Why every listed node has to be on every path: the engine reads the newest
|
|
817
|
+
* round of approvals of a node, and a return can send the instance back to the
|
|
818
|
+
* start. A listed node that this round did not pass would leave its previous
|
|
819
|
+
* round's approver in the exclusion list, and the node would refuse a person
|
|
820
|
+
* who never signed an earlier stage in this round. Requiring a listed node to
|
|
821
|
+
* be run, and signed by a person, every time rules that out.
|
|
822
|
+
*
|
|
823
|
+
* The `NODE_ACTOR` rule "two sources on different branches of one parallel
|
|
824
|
+
* split" is not applied a second time. A listed node that sits on a parallel
|
|
825
|
+
* branch is still refused by the coverage rule above, though: coverage reads a
|
|
826
|
+
* parallel gateway like an exclusive one, the same limit `NODE_ACTOR` has, so
|
|
827
|
+
* such stages have to be chained one after another instead.
|
|
828
|
+
*/
|
|
829
|
+
function readExcludeNodeActorsIssues(definition, nodeId, config) {
|
|
830
|
+
if (config === undefined) {
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
const shapeIssue = readExcludeNodeActorsShapeIssue(config);
|
|
834
|
+
if (shapeIssue !== null) {
|
|
835
|
+
return [shapeIssue];
|
|
836
|
+
}
|
|
837
|
+
const sourceNodeIds = config.nodeIds;
|
|
838
|
+
if (sourceNodeIds.includes(nodeId)) {
|
|
839
|
+
return ['「不可與前面關卡同一人」所選的前面關卡不可以是自己。'];
|
|
840
|
+
}
|
|
841
|
+
const labelOf = (id) => {
|
|
842
|
+
const node = definition.nodes.find((candidate) => candidate.id === id);
|
|
843
|
+
return (node === null || node === void 0 ? void 0 : node.data.label) ? `「${node.data.label}」` : `「${id}」`;
|
|
844
|
+
};
|
|
845
|
+
const sourceNodes = sourceNodeIds.map((id) => definition.nodes.find((candidate) => candidate.id === id));
|
|
846
|
+
const missingId = sourceNodeIds.find((id, index) => sourceNodes[index] === undefined);
|
|
847
|
+
if (missingId !== undefined) {
|
|
848
|
+
return [`「不可與前面關卡同一人」所選的前面關卡「${missingId}」不存在。`];
|
|
849
|
+
}
|
|
850
|
+
const notUserTaskId = sourceNodeIds.find((id, index) => { var _a; return ((_a = sourceNodes[index]) === null || _a === void 0 ? void 0 : _a.type) !== 'userTask'; });
|
|
851
|
+
if (notUserTaskId !== undefined) {
|
|
852
|
+
return [
|
|
853
|
+
`「不可與前面關卡同一人」所選的前面關卡 ${labelOf(notUserTaskId)} 不是簽核關卡。`,
|
|
854
|
+
];
|
|
855
|
+
}
|
|
856
|
+
const stageIssues = readSourceStageIssues(definition, nodeId, sourceNodeIds, sourceNodeIds.map((id) => [id]), EXCLUDE_NODE_ACTORS_STAGE_TEXTS);
|
|
857
|
+
if (stageIssues.length > 0) {
|
|
858
|
+
return stageIssues;
|
|
859
|
+
}
|
|
860
|
+
const self = definition.nodes.find((node) => node.id === nodeId);
|
|
861
|
+
return config.whenEmpty === 'SKIP_NODE' &&
|
|
862
|
+
(self === null || self === void 0 ? void 0 : self.type) === 'userTask' &&
|
|
863
|
+
(0, field_access_1.readNodeFillableFieldKeys)(self).size > 0
|
|
864
|
+
? [
|
|
865
|
+
`簽核節點 ${labelOf(nodeId)} 有可由簽核人填寫的欄位,「不可與前面關卡同一人」不能選擇略過此節點。`,
|
|
866
|
+
]
|
|
867
|
+
: [];
|
|
868
|
+
}
|
|
753
869
|
/**
|
|
754
870
|
* `true` when an `ORG_MANAGER_CHAIN` node uses a decision policy that lets one
|
|
755
871
|
* manager close the node (`SINGLE`, `PARALLEL_ANY`, `QUORUM`) or has none at
|
|
@@ -765,6 +881,57 @@ function isOrgManagerChainDecisionPolicyInvalid(resolver, decisionPolicy) {
|
|
|
765
881
|
(decisionPolicy === null || decisionPolicy === void 0 ? void 0 : decisionPolicy.type) !== 'SEQUENTIAL' &&
|
|
766
882
|
(decisionPolicy === null || decisionPolicy === void 0 ? void 0 : decisionPolicy.type) !== 'PARALLEL_ALL');
|
|
767
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* The designer-facing issue with the two optional settings of an
|
|
886
|
+
* `ORG_MANAGER_CHAIN` (`minPositionLevel`, `startFrom`, `startPositionLevel`),
|
|
887
|
+
* or `null` when all are well-formed. `subject` names the node kind in the
|
|
888
|
+
* sentence; a notify node's chain may only start at the initiator (it has no
|
|
889
|
+
* earlier stage and no form pick to start from). Reads defensively: on the
|
|
890
|
+
* backend the definition is parsed JSON that no DTO has validated. The form
|
|
891
|
+
* field and the earlier stages the start names are checked elsewhere, against
|
|
892
|
+
* the form schema and the graph.
|
|
893
|
+
*/
|
|
894
|
+
function readOrgManagerChainIssue(resolver, subject) {
|
|
895
|
+
// Checked first, for both subjects: without a whole-number level the climb
|
|
896
|
+
// never stops (`level >= undefined` is always false) and collects every
|
|
897
|
+
// manager up to the top.
|
|
898
|
+
if (!Number.isInteger(resolver.minPositionLevel)) {
|
|
899
|
+
return `${subject}需要指定有效的職等門檻。`;
|
|
900
|
+
}
|
|
901
|
+
const startFrom = resolver.startFrom;
|
|
902
|
+
if (startFrom !== undefined && startFrom !== null) {
|
|
903
|
+
const start = startFrom;
|
|
904
|
+
if (typeof start !== 'object' ||
|
|
905
|
+
typeof start.type !== 'string' ||
|
|
906
|
+
(0, workflow_1.readUnsupportedOrgManagerChainStartType)({ type: start.type }) !== null) {
|
|
907
|
+
return `${subject}的主管鏈起點不在這個版本支援的範圍內,請改選其他起點。`;
|
|
908
|
+
}
|
|
909
|
+
if (start.type === 'FORM_MEMBER' &&
|
|
910
|
+
(typeof start.formPath !== 'string' || !start.formPath.trim())) {
|
|
911
|
+
return `${subject}需要指定主管鏈起點的表單欄位。`;
|
|
912
|
+
}
|
|
913
|
+
if (start.type === 'NODE_ACTOR' &&
|
|
914
|
+
(!Array.isArray(start.nodeIds) || start.nodeIds.length === 0)) {
|
|
915
|
+
return `${subject}需要指定主管鏈起點的前面簽核關卡。`;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (subject === '知會節點' &&
|
|
919
|
+
!(0, workflow_1.isOrgManagerChainStartedFromInitiator)(resolver)) {
|
|
920
|
+
return '知會節點的主管鏈只能從發起人開始。';
|
|
921
|
+
}
|
|
922
|
+
const rawStartLevel = resolver.startPositionLevel;
|
|
923
|
+
if (rawStartLevel === undefined || rawStartLevel === null) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const startLevel = (0, workflow_1.readOrgManagerChainStartPositionLevel)(resolver);
|
|
927
|
+
if (startLevel === null || !Number.isInteger(startLevel)) {
|
|
928
|
+
return `${subject}需要指定有效的起始職等。`;
|
|
929
|
+
}
|
|
930
|
+
return Number.isInteger(resolver.minPositionLevel) &&
|
|
931
|
+
startLevel > resolver.minPositionLevel
|
|
932
|
+
? `${subject}的起始職等不可高於職等門檻。`
|
|
933
|
+
: null;
|
|
934
|
+
}
|
|
768
935
|
function readApproverResolverIssue(resolver) {
|
|
769
936
|
var _a, _b;
|
|
770
937
|
if ((0, workflow_1.readUnsupportedApproverResolverType)(resolver) !== null) {
|
|
@@ -786,9 +953,12 @@ function readApproverResolverIssue(resolver) {
|
|
|
786
953
|
(!resolver.orgUnitId.trim() || !resolver.positionId.trim())) {
|
|
787
954
|
return '簽核節點需要指定組織與職位。';
|
|
788
955
|
}
|
|
789
|
-
if (resolver.type === 'ORG_MANAGER_CHAIN'
|
|
790
|
-
|
|
791
|
-
|
|
956
|
+
if (resolver.type === 'ORG_MANAGER_CHAIN') {
|
|
957
|
+
// The level is checked first inside `readOrgManagerChainIssue`.
|
|
958
|
+
const chainIssue = readOrgManagerChainIssue(resolver, '簽核節點');
|
|
959
|
+
if (chainIssue !== null) {
|
|
960
|
+
return chainIssue;
|
|
961
|
+
}
|
|
792
962
|
}
|
|
793
963
|
if ((resolver.type === 'FORM_MEMBER_MANAGER' ||
|
|
794
964
|
resolver.type === 'FORM_ORG_UNIT_MANAGER') &&
|
|
@@ -826,13 +996,16 @@ function readApproverResolverIssue(resolver) {
|
|
|
826
996
|
return null;
|
|
827
997
|
}
|
|
828
998
|
function readNodeActorSourceSkipReasons(node) {
|
|
829
|
-
var _a, _b, _c;
|
|
999
|
+
var _a, _b, _c, _d;
|
|
830
1000
|
return [
|
|
831
1001
|
...(((_a = node.data.entryCondition) === null || _a === void 0 ? void 0 : _a.trim()) ? ['entryCondition'] : []),
|
|
832
1002
|
...(((_b = node.data.excludeInitiator) === null || _b === void 0 ? void 0 : _b.whenEmpty) === 'SKIP_NODE'
|
|
833
1003
|
? ['skipNode']
|
|
834
1004
|
: []),
|
|
835
|
-
...(((_c = node.data.
|
|
1005
|
+
...(((_c = node.data.excludeNodeActors) === null || _c === void 0 ? void 0 : _c.whenEmpty) === 'SKIP_NODE'
|
|
1006
|
+
? ['skipNodeByNodeActors']
|
|
1007
|
+
: []),
|
|
1008
|
+
...(((_d = node.data.sla) === null || _d === void 0 ? void 0 : _d.onTimeout) === 'AUTO_APPROVE'
|
|
836
1009
|
? ['autoApprove']
|
|
837
1010
|
: []),
|
|
838
1011
|
];
|
|
@@ -841,7 +1014,61 @@ const NODE_ACTOR_SOURCE_SKIP_ISSUES = {
|
|
|
841
1014
|
autoApprove: (label) => `簽核來源的前面關卡 ${label} 設有逾時自動核准,可能沒有人實際簽核。`,
|
|
842
1015
|
entryCondition: (label) => `簽核來源的前面關卡 ${label} 設有進入條件,可能被跳過、沒有人簽核。`,
|
|
843
1016
|
skipNode: (label) => `簽核來源的前面關卡 ${label} 在排除發起人後沒人時會跳過此關,可能沒有人簽核。`,
|
|
1017
|
+
skipNodeByNodeActors: (label) => `簽核來源的前面關卡 ${label} 在排除前面關卡的簽核者後沒人時會跳過此關,可能沒有人簽核。`,
|
|
1018
|
+
};
|
|
1019
|
+
const NODE_ACTOR_SOURCE_STAGE_TEXTS = {
|
|
1020
|
+
coverage: (stageLabels, nodeLabel) => `有一條路徑不會經過所選的前面關卡(${stageLabels.join('、')})就走到 ${nodeLabel},那一趟沒有人可以當簽核人。`,
|
|
1021
|
+
notUpstream: (stageLabel, nodeLabel) => `前面關卡 ${stageLabel} 不在 ${nodeLabel} 的上游,走不到這個節點,不能當簽核來源。`,
|
|
1022
|
+
skip: NODE_ACTOR_SOURCE_SKIP_ISSUES,
|
|
844
1023
|
};
|
|
1024
|
+
const EXCLUDE_NODE_ACTORS_STAGE_TEXTS = {
|
|
1025
|
+
coverage: (stageLabels, nodeLabel) => `有一條路徑不會經過排除對象的前面關卡 ${stageLabels.join('、')} 就走到 ${nodeLabel},那一趟排除不到本輪的簽核者(退回重送後還會讀到上一輪的簽核者)。請改用分流,把兩關放在同一條路線上。`,
|
|
1026
|
+
notUpstream: (stageLabel, nodeLabel) => `前面關卡 ${stageLabel} 不在 ${nodeLabel} 的上游,走不到這個節點,無法排除該關的簽核者。`,
|
|
1027
|
+
skip: {
|
|
1028
|
+
autoApprove: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 設有逾時自動核准,可能沒有人實際簽核,這一關就排除不到任何人。請移除逾時自動核准,或改選其他關卡。`,
|
|
1029
|
+
entryCondition: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 設有進入條件,可能被跳過;被跳過時會改讀到上一輪的簽核者。請改用分流,把兩關放在同一條路線上。`,
|
|
1030
|
+
skipNode: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 在排除發起人後沒人時會跳過此關,可能被跳過;被跳過時會改讀到上一輪的簽核者。請改選其他處理方式。`,
|
|
1031
|
+
skipNodeByNodeActors: (stageLabel) => `排除對象的前面關卡 ${stageLabel} 在排除前面關卡的簽核者後沒人時會跳過此關,可能被跳過;被跳過時會改讀到上一輪的簽核者。請改選其他處理方式。`,
|
|
1032
|
+
},
|
|
1033
|
+
};
|
|
1034
|
+
/**
|
|
1035
|
+
* The checks every "who signed an earlier stage" reader needs, shared by the
|
|
1036
|
+
* `NODE_ACTOR` source ({@link readNodeActorSourceIssues}) and
|
|
1037
|
+
* `excludeNodeActors` ({@link readExcludeNodeActorsIssues}). In this order,
|
|
1038
|
+
* all of them reported together:
|
|
1039
|
+
*
|
|
1040
|
+
* 1. a stage that may be skipped or pass without a signer
|
|
1041
|
+
* ({@link readNodeActorSourceSkipReasons});
|
|
1042
|
+
* 2. coverage: for each group in `coverageGroups`, walking from the start
|
|
1043
|
+
* without entering any stage of the group must not reach `nodeId`. A
|
|
1044
|
+
* `NODE_ACTOR` source passes its whole list as one group ("one of them
|
|
1045
|
+
* covers every path"); an exclusion passes one group per stage;
|
|
1046
|
+
* 3. every stage is upstream of `nodeId`.
|
|
1047
|
+
*
|
|
1048
|
+
* The caller has already checked that every id exists and is a `userTask`.
|
|
1049
|
+
*/
|
|
1050
|
+
function readSourceStageIssues(definition, nodeId, stageIds, coverageGroups, texts) {
|
|
1051
|
+
const labelOf = (id) => {
|
|
1052
|
+
const node = definition.nodes.find((candidate) => candidate.id === id);
|
|
1053
|
+
return (node === null || node === void 0 ? void 0 : node.data.label) ? `「${node.data.label}」` : `「${id}」`;
|
|
1054
|
+
};
|
|
1055
|
+
const skippableIssues = stageIds.flatMap((id) => {
|
|
1056
|
+
const node = definition.nodes.find((candidate) => candidate.id === id);
|
|
1057
|
+
return (node === null || node === void 0 ? void 0 : node.type) === 'userTask'
|
|
1058
|
+
? readNodeActorSourceSkipReasons(node).map((reason) => texts.skip[reason](labelOf(node.id)))
|
|
1059
|
+
: [];
|
|
1060
|
+
});
|
|
1061
|
+
const startNodeIds = definition.nodes
|
|
1062
|
+
.filter((node) => node.type === 'startEvent')
|
|
1063
|
+
.map((node) => node.id);
|
|
1064
|
+
const coverageIssues = coverageGroups.flatMap((group) => (0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, startNodeIds, new Set(group)).has(nodeId)
|
|
1065
|
+
? [texts.coverage(group.map(labelOf), labelOf(nodeId))]
|
|
1066
|
+
: []);
|
|
1067
|
+
const notUpstreamIssues = stageIds
|
|
1068
|
+
.filter((id) => !(0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, [id]).has(nodeId))
|
|
1069
|
+
.map((id) => texts.notUpstream(labelOf(id), labelOf(nodeId)));
|
|
1070
|
+
return [...skippableIssues, ...coverageIssues, ...notUpstreamIssues];
|
|
1071
|
+
}
|
|
845
1072
|
/**
|
|
846
1073
|
* The graph checks behind a `NODE_ACTOR` / `NODE_ACTOR_MANAGER` node: the
|
|
847
1074
|
* "who signed earlier" source only works when every case reaches `nodeId`
|
|
@@ -886,34 +1113,88 @@ function readNodeActorSourceIssues(definition, nodeId, sourceNodeIds) {
|
|
|
886
1113
|
if (notUserTaskId !== undefined) {
|
|
887
1114
|
return [`簽核來源的前面關卡 ${labelOf(notUserTaskId)} 不是簽核關卡。`];
|
|
888
1115
|
}
|
|
889
|
-
const
|
|
890
|
-
? readNodeActorSourceSkipReasons(node).map((reason) => NODE_ACTOR_SOURCE_SKIP_ISSUES[reason](labelOf(node.id)))
|
|
891
|
-
: []);
|
|
892
|
-
const startNodeIds = definition.nodes
|
|
893
|
-
.filter((node) => node.type === 'startEvent')
|
|
894
|
-
.map((node) => node.id);
|
|
895
|
-
const reachableWithoutSource = (0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, startNodeIds, new Set(sourceNodeIds));
|
|
896
|
-
const coverageIssues = reachableWithoutSource.has(nodeId)
|
|
897
|
-
? [
|
|
898
|
-
`有一條路徑不會經過所選的前面關卡(${sourceNodeIds
|
|
899
|
-
.map(labelOf)
|
|
900
|
-
.join('、')})就走到 ${labelOf(nodeId)},那一趟沒有人可以當簽核人。`,
|
|
901
|
-
]
|
|
902
|
-
: [];
|
|
903
|
-
const notUpstreamIssues = sourceNodeIds
|
|
904
|
-
.filter((id) => !(0, workflow_reachability_1.readReachableWorkflowNodeIds)(definition.edges, [id]).has(nodeId))
|
|
905
|
-
.map((id) => `前面關卡 ${labelOf(id)} 不在 ${labelOf(nodeId)} 的上游,走不到這個節點,不能當簽核來源。`);
|
|
1116
|
+
const stageIssues = readSourceStageIssues(definition, nodeId, sourceNodeIds, [sourceNodeIds], NODE_ACTOR_SOURCE_STAGE_TEXTS);
|
|
906
1117
|
const parallelBranches = (0, workflow_reachability_1.readWorkflowParallelBranchSets)(definition);
|
|
907
1118
|
const parallelIssues = sourceNodeIds.flatMap((id, index) => sourceNodeIds
|
|
908
1119
|
.slice(index + 1)
|
|
909
1120
|
.filter((otherId) => parallelBranches.some((branches) => (0, workflow_reachability_1.isOnOwnParallelBranch)(branches, id, otherId)))
|
|
910
1121
|
.map((otherId) => `前面關卡 ${labelOf(id)} 與 ${labelOf(otherId)} 在平行分支的不同路線上,兩關會同時簽核,不能一起當簽核來源。`));
|
|
911
|
-
return [
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1122
|
+
return [...stageIssues, ...parallelIssues];
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* `true` when a user task can end with more than one person having approved
|
|
1126
|
+
* it, judged from its definition alone. The engine reads "who signed this
|
|
1127
|
+
* stage" as the people with an approval in its newest batch, so a source that
|
|
1128
|
+
* several people approve gives a start that is not one person.
|
|
1129
|
+
*
|
|
1130
|
+
* - `SINGLE` and `PARALLEL_ANY` end on the first approval: one person;
|
|
1131
|
+
* - `PARALLEL_ALL` and `SEQUENTIAL` wait for every seat: several;
|
|
1132
|
+
* - `QUORUM` ends once the threshold is met: several when a `COUNT` threshold
|
|
1133
|
+
* is above 1 (the engine reads anything below 1 as 1), and always for a
|
|
1134
|
+
* `PERCENTAGE` threshold, whose head count only the org chart at run time
|
|
1135
|
+
* knows;
|
|
1136
|
+
* - a `DIRECT` resolver that lists one member can only ever give one person,
|
|
1137
|
+
* whatever the policy ({@link readDesignTimeApproverCount}), unless the
|
|
1138
|
+
* stage keeps the initiator (`excludeInitiator`) or an earlier stage's
|
|
1139
|
+
* approvers (`excludeNodeActors`) out with `whenEmpty: 'INITIATOR_MANAGER'`:
|
|
1140
|
+
* when that one member is excluded the stage goes to the initiator's
|
|
1141
|
+
* manager instead, who may be several people, so the policy decides.
|
|
1142
|
+
*
|
|
1143
|
+
* Deliberately conservative on the unknown side: a policy this build does not
|
|
1144
|
+
* recognise reads as one person, because the structural lint reports it.
|
|
1145
|
+
* The designer's stage picker for a manager chain's `NODE_ACTOR` start reads
|
|
1146
|
+
* this too, so it and {@link readSingleApproverSourceIssues} agree.
|
|
1147
|
+
*/
|
|
1148
|
+
function canUserTaskHaveMultipleApprovers(node) {
|
|
1149
|
+
var _a, _b;
|
|
1150
|
+
// `approverResolver` and `decisionPolicy` can be missing on raw JSON that has
|
|
1151
|
+
// not been linted yet.
|
|
1152
|
+
const resolver = node.data.approverResolver;
|
|
1153
|
+
const policy = node.data.decisionPolicy;
|
|
1154
|
+
const designTimeCount = resolver
|
|
1155
|
+
? readDesignTimeApproverCount(resolver)
|
|
1156
|
+
: null;
|
|
1157
|
+
// Who the initiator's manager is (and how many share the level) is only known
|
|
1158
|
+
// at run time, so a lone listed member is no guarantee of one person here.
|
|
1159
|
+
// Either exclusion can empty the stage and hand it over.
|
|
1160
|
+
const mayHandOverToInitiatorManagers = ((_a = node.data.excludeInitiator) === null || _a === void 0 ? void 0 : _a.whenEmpty) === 'INITIATOR_MANAGER' ||
|
|
1161
|
+
((_b = node.data.excludeNodeActors) === null || _b === void 0 ? void 0 : _b.whenEmpty) === 'INITIATOR_MANAGER';
|
|
1162
|
+
if (!mayHandOverToInitiatorManagers &&
|
|
1163
|
+
designTimeCount !== null &&
|
|
1164
|
+
designTimeCount <= 1) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
if ((policy === null || policy === void 0 ? void 0 : policy.type) === 'PARALLEL_ALL' || (policy === null || policy === void 0 ? void 0 : policy.type) === 'SEQUENTIAL') {
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
1170
|
+
return ((policy === null || policy === void 0 ? void 0 : policy.type) === 'QUORUM' &&
|
|
1171
|
+
(policy.thresholdType === 'PERCENTAGE' || policy.threshold > 1));
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* The publish-time issues with the stages a manager chain's `NODE_ACTOR`
|
|
1175
|
+
* start names, beyond what {@link readNodeActorSourceIssues} checks: every
|
|
1176
|
+
* stage must end with one person's approval. A stage several people approve
|
|
1177
|
+
* would make the start ambiguous, and the engine can only say so when the last
|
|
1178
|
+
* of them approves, inside that decision's own transaction, which then never
|
|
1179
|
+
* commits. Returns Chinese issue lines; `[]` means every named stage is a
|
|
1180
|
+
* single-signer stage.
|
|
1181
|
+
*
|
|
1182
|
+
* `NODE_ACTOR_MANAGER` takes the managers of everyone who signed and so has no
|
|
1183
|
+
* such limit; this check is deliberately not part of
|
|
1184
|
+
* {@link readNodeActorSourceIssues}. A stage that does not exist or is not a
|
|
1185
|
+
* user task is reported there, not here.
|
|
1186
|
+
*/
|
|
1187
|
+
function readSingleApproverSourceIssues(definition, sourceNodeIds) {
|
|
1188
|
+
return sourceNodeIds.flatMap((id) => {
|
|
1189
|
+
const node = definition.nodes.find((candidate) => candidate.id === id);
|
|
1190
|
+
if ((node === null || node === void 0 ? void 0 : node.type) !== 'userTask' || !canUserTaskHaveMultipleApprovers(node)) {
|
|
1191
|
+
return [];
|
|
1192
|
+
}
|
|
1193
|
+
const label = node.data.label ? `「${node.data.label}」` : `「${id}」`;
|
|
1194
|
+
return [
|
|
1195
|
+
`主管鏈起點的前面關卡 ${label} 可能有多位實際簽核人,無法決定從誰的主管開始。請改選只會有一位簽核人的關卡。`,
|
|
1196
|
+
];
|
|
1197
|
+
});
|
|
917
1198
|
}
|
|
918
1199
|
function hasConfiguredConditionEdges(definition) {
|
|
919
1200
|
return definition.edges.some((edge) => isExclusiveGatewaySourceEdge(edge, definition.nodes) &&
|
|
@@ -988,12 +1269,20 @@ function readNotifyRecipientsIssue(recipients) {
|
|
|
988
1269
|
(!recipients.orgUnitId.trim() || !recipients.positionId.trim())) {
|
|
989
1270
|
return '知會節點需要指定組織與職位。';
|
|
990
1271
|
}
|
|
1272
|
+
// Hand-written or AI-written JSON may carry no `orgUnitId` at all.
|
|
1273
|
+
if (recipients.type === 'ORG_UNIT_MANAGER' &&
|
|
1274
|
+
(typeof recipients.orgUnitId !== 'string' || !recipients.orgUnitId.trim())) {
|
|
1275
|
+
return '知會節點需要指定組織。';
|
|
1276
|
+
}
|
|
991
1277
|
if (recipients.type === 'DYNAMIC_FORM' && !recipients.formPath.trim()) {
|
|
992
1278
|
return '知會節點需要指定知會對象的表單欄位。';
|
|
993
1279
|
}
|
|
994
1280
|
if (recipients.type === 'EXPRESSION' && !recipients.expression.trim()) {
|
|
995
1281
|
return '知會節點需要指定知會對象的運算式。';
|
|
996
1282
|
}
|
|
1283
|
+
if (recipients.type === 'ORG_MANAGER_CHAIN') {
|
|
1284
|
+
return readOrgManagerChainIssue(recipients, '知會節點');
|
|
1285
|
+
}
|
|
997
1286
|
return null;
|
|
998
1287
|
}
|
|
999
1288
|
/**
|