@bpmn-nova/studio 0.3.0-preview → 0.3.2-preview
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/README.md +245 -20
- package/dist/canvas.js +7 -4
- package/dist/context-menu.js +2 -2
- package/dist/controller.js +84 -6
- package/dist/index.d.ts +138 -38
- package/dist/index.js +12 -11
- package/dist/interactions.js +1 -1
- package/dist/modules/bpmn-model/index.d.ts +11 -0
- package/dist/modules/bpmn-model/index.js +703 -0
- package/dist/modules/core/containment.js +590 -0
- package/dist/modules/core/gateway.js +72 -0
- package/dist/modules/core/history.js +32 -0
- package/dist/modules/core/index.d.ts +268 -0
- package/dist/modules/core/index.js +7 -0
- package/dist/modules/core/layout.js +644 -0
- package/dist/modules/core/model.js +287 -0
- package/dist/modules/core/runtime-transition-route.js +360 -0
- package/dist/modules/core/scope.js +99 -0
- package/dist/modules/designer/index.d.ts +79 -0
- package/dist/modules/designer/index.js +607 -0
- package/dist/modules/engine-activiti/index.d.ts +19 -0
- package/dist/modules/engine-activiti/index.js +160 -0
- package/dist/modules/engine-flowable/index.d.ts +19 -0
- package/dist/modules/engine-flowable/index.js +160 -0
- package/dist/modules/export-svg/index.d.ts +112 -0
- package/dist/modules/export-svg/index.js +2 -0
- package/dist/modules/export-svg/preview.js +327 -0
- package/dist/modules/export-svg/render.js +718 -0
- package/dist/modules/icons/index.d.ts +24 -0
- package/dist/modules/icons/index.js +264 -0
- package/dist/modules/palette/index.d.ts +74 -0
- package/dist/modules/palette/index.js +99 -0
- package/dist/modules/palette/panel.js +99 -0
- package/dist/modules/properties/index.d.ts +20 -0
- package/dist/modules/properties/index.js +19 -0
- package/dist/modules/properties-activiti/index.d.ts +3 -0
- package/dist/modules/properties-activiti/index.js +97 -0
- package/dist/modules/properties-bpmn/index.d.ts +3 -0
- package/dist/modules/properties-bpmn/index.js +518 -0
- package/dist/modules/properties-core/index.d.ts +124 -0
- package/dist/modules/properties-core/index.js +312 -0
- package/dist/modules/properties-flowable/index.d.ts +3 -0
- package/dist/modules/properties-flowable/index.js +114 -0
- package/dist/modules/properties-renderer/index.d.ts +25 -0
- package/dist/modules/properties-renderer/index.js +491 -0
- package/dist/modules/renderer-svg/index.d.ts +118 -0
- package/dist/modules/renderer-svg/index.js +1460 -0
- package/dist/modules/runtime/index.d.ts +169 -0
- package/dist/modules/runtime/index.js +535 -0
- package/dist/modules/theme/index.d.ts +95 -0
- package/dist/modules/theme/index.js +368 -0
- package/dist/modules/viewer/index.d.ts +265 -0
- package/dist/modules/viewer/index.js +1011 -0
- package/dist/modules/viewer/runtime-content.js +123 -0
- package/dist/modules/viewer/runtime-details-motion.js +228 -0
- package/dist/modules/viewer/runtime-trace.js +574 -0
- package/dist/modules/viewer/timeline.js +276 -0
- package/dist/selection-layout.js +1 -1
- package/dist/shell.js +210 -26
- package/dist/styles.css +116 -7
- package/llms-full.txt +3082 -0
- package/llms.txt +225 -0
- package/package.json +39 -16
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import {
|
|
2
|
+
beautify,
|
|
3
|
+
cloneModel,
|
|
4
|
+
createEdge,
|
|
5
|
+
EMBEDDED_SUBPROCESS_TYPES,
|
|
6
|
+
getScopeGraph,
|
|
7
|
+
NODE_DEFINITIONS,
|
|
8
|
+
} from '../core/index.js';
|
|
9
|
+
import { createRuntimePresentation, normalizeRuntime } from '../runtime/index.js';
|
|
10
|
+
|
|
11
|
+
const HUMAN_TASKS = new Set(['userTask', 'manualTask', 'callActivity']);
|
|
12
|
+
const AUTOMATED_TASKS = new Set(['serviceTask', 'scriptTask', 'businessRuleTask', 'sendTask', 'receiveTask']);
|
|
13
|
+
const CONDITIONAL_GATEWAYS = new Set(['exclusiveGateway', 'inclusiveGateway', 'eventBasedGateway', 'complexGateway']);
|
|
14
|
+
const ABNORMAL_TRANSITIONS = new Set(['reject', 'return']);
|
|
15
|
+
|
|
16
|
+
function timestamp(value, fallback = 0) {
|
|
17
|
+
if (!value) return fallback;
|
|
18
|
+
const parsed = Date.parse(String(value).replace(' ', 'T'));
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function recordOrder(record, index = 0) {
|
|
23
|
+
return timestamp(record.startTime || record.endTime, index);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function transitionOrder(transition, index = 0) {
|
|
27
|
+
return timestamp(transition.occurredAt || transition.time, index);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function edgeVisitOrder(visit, index = 0) {
|
|
31
|
+
return timestamp(visit.occurredAt || visit.time, index);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function visitStatus(records) {
|
|
35
|
+
if (records.some((record) => ['rejected', 'returned'].includes(record.outcome))) return 'failed';
|
|
36
|
+
if (records.some((record) => record.status === 'failed')) return 'failed';
|
|
37
|
+
if (records.some((record) => record.status === 'active')) return 'active';
|
|
38
|
+
if (records.every((record) => record.status === 'skipped')) return 'skipped';
|
|
39
|
+
if (records.some((record) => record.status === 'cancelled') && !records.some((record) => record.status === 'completed')) return 'cancelled';
|
|
40
|
+
return 'completed';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function statusLabel(status, records) {
|
|
44
|
+
const outcome = records.map((record) => record.outcome).filter(Boolean).at(-1);
|
|
45
|
+
if (outcome === 'rejected') return '已驳回';
|
|
46
|
+
if (outcome === 'returned') return '已退回';
|
|
47
|
+
const participants = records.map((record) => record.participant).filter((participant) => participant?.name);
|
|
48
|
+
const completed = records.filter((record) => record.status === 'completed').length;
|
|
49
|
+
const total = Math.max(records.reduce((value, record) => Math.max(value, Number(record.totalInstances) || 0), 0), records.length, participants.length);
|
|
50
|
+
const approvalMode = records.find((record) => record.approvalMode)?.approvalMode;
|
|
51
|
+
const multiInstanceMode = records.find((record) => record.multiInstanceMode)?.multiInstanceMode;
|
|
52
|
+
if (approvalMode === 'all') return `${multiInstanceMode === 'sequential' ? '顺序会签' : '会签'} ${completed}/${total}`;
|
|
53
|
+
if (approvalMode === 'any') return `或签 ${completed}/${total}`;
|
|
54
|
+
if (participants.length > 1) return `多人审批 ${completed}/${total}`;
|
|
55
|
+
return { active: '处理中', completed: '已完成', failed: '失败', cancelled: '已取消', skipped: '未经过', idle: '未到达' }[status] || status;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function uniqueParticipants(records) {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
return records.map((record) => record.participant).filter((participant) => {
|
|
61
|
+
if (!participant?.name) return false;
|
|
62
|
+
const key = participant.id || participant.name;
|
|
63
|
+
if (seen.has(key)) return false;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function participantSummary(participants) {
|
|
70
|
+
const visible = participants.slice(0, 2).map((participant) => participant.name).join('、');
|
|
71
|
+
return participants.length > 2 ? `${visible} +${participants.length - 2}` : visible;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isStartOrEnd(node) {
|
|
75
|
+
const stage = NODE_DEFINITIONS[node?.type]?.eventStage;
|
|
76
|
+
return stage === 'start' || stage === 'end';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPlainMilestone(node, stage) {
|
|
80
|
+
const definition = NODE_DEFINITIONS[node?.type];
|
|
81
|
+
return definition?.eventStage === stage && !definition.eventDefinition;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isGateway(node) {
|
|
85
|
+
return NODE_DEFINITIONS[node?.type]?.kind === 'gateway';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isVisibleTraceNode(node, {
|
|
89
|
+
actual = false,
|
|
90
|
+
predicted = false,
|
|
91
|
+
showStartMilestone = false,
|
|
92
|
+
showEndMilestone = false,
|
|
93
|
+
} = {}) {
|
|
94
|
+
if (!node) return false;
|
|
95
|
+
if (isPlainMilestone(node, 'start')) return showStartMilestone && (actual || predicted);
|
|
96
|
+
if (isPlainMilestone(node, 'end')) return showEndMilestone && (actual || predicted);
|
|
97
|
+
if (isStartOrEnd(node)) return actual || predicted;
|
|
98
|
+
if (EMBEDDED_SUBPROCESS_TYPES.has(node.type)) return actual || predicted;
|
|
99
|
+
if (HUMAN_TASKS.has(node.type)) return actual || predicted;
|
|
100
|
+
if (AUTOMATED_TASKS.has(node.type)) return actual;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function rootTraceElementId(model, elementId, rootNodeIds, nodeMap) {
|
|
105
|
+
if (rootNodeIds.has(elementId)) return elementId;
|
|
106
|
+
const node = nodeMap.get(elementId);
|
|
107
|
+
if (!node) return null;
|
|
108
|
+
let scopeId = node.scopeId || model.id;
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
while (scopeId && scopeId !== model.id && !seen.has(scopeId)) {
|
|
111
|
+
seen.add(scopeId);
|
|
112
|
+
const owner = nodeMap.get(scopeId);
|
|
113
|
+
if (!owner || !EMBEDDED_SUBPROCESS_TYPES.has(owner.type)) return null;
|
|
114
|
+
if ((owner.scopeId || model.id) === model.id) return owner.id;
|
|
115
|
+
scopeId = owner.scopeId || model.id;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function collapseRuntimeToRoot(model, rootModel, runtime) {
|
|
121
|
+
const rootNodeIds = new Set(rootModel.nodes.map((node) => node.id));
|
|
122
|
+
const nodeMap = new Map(model.nodes.map((node) => [node.id, node]));
|
|
123
|
+
const activitySources = new Map();
|
|
124
|
+
const activityVisitSources = new Map();
|
|
125
|
+
const activities = runtime.activities.map((record) => {
|
|
126
|
+
const rootElementId = rootTraceElementId(model, record.elementId, rootNodeIds, nodeMap);
|
|
127
|
+
if (!rootElementId || rootElementId === record.elementId) return record;
|
|
128
|
+
if (!activitySources.has(rootElementId)) activitySources.set(rootElementId, new Set());
|
|
129
|
+
activitySources.get(rootElementId).add(record.elementId);
|
|
130
|
+
if (record.id && record.visitId) activityVisitSources.set(record.id, record.visitId);
|
|
131
|
+
return {
|
|
132
|
+
...record,
|
|
133
|
+
elementId: rootElementId,
|
|
134
|
+
visitId: `subprocess:${rootElementId}`,
|
|
135
|
+
multiInstanceId: undefined,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
const transitions = runtime.transitions.map((transition) => {
|
|
139
|
+
const sourceElementId = rootTraceElementId(model, transition.sourceElementId, rootNodeIds, nodeMap) || transition.sourceElementId;
|
|
140
|
+
const targetElementId = rootTraceElementId(model, transition.targetElementId, rootNodeIds, nodeMap) || transition.targetElementId;
|
|
141
|
+
return { ...transition, sourceElementId, targetElementId };
|
|
142
|
+
}).filter((transition) => transition.sourceElementId !== transition.targetElementId);
|
|
143
|
+
return {
|
|
144
|
+
runtime: { ...runtime, activities, transitions },
|
|
145
|
+
activitySources,
|
|
146
|
+
activityVisitSources,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function graphMaps(model) {
|
|
151
|
+
const nodes = new Map(model.nodes.map((node) => [node.id, node]));
|
|
152
|
+
const edges = new Map(model.edges.map((edge) => [edge.id, edge]));
|
|
153
|
+
const outgoing = new Map(model.nodes.map((node) => [node.id, []]));
|
|
154
|
+
for (const edge of model.edges) {
|
|
155
|
+
if (!outgoing.has(edge.source)) outgoing.set(edge.source, []);
|
|
156
|
+
outgoing.get(edge.source).push(edge);
|
|
157
|
+
}
|
|
158
|
+
return { nodes, edges, outgoing };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function findPath(outgoing, sourceId, targetId, allowed = null, maxDepth = 100) {
|
|
162
|
+
if (!sourceId || !targetId || sourceId === targetId) return [];
|
|
163
|
+
const queue = [{ id: sourceId, edges: [], seen: new Set([sourceId]) }];
|
|
164
|
+
const found = [];
|
|
165
|
+
while (queue.length && found.length < 2) {
|
|
166
|
+
const current = queue.shift();
|
|
167
|
+
if (current.edges.length >= maxDepth) continue;
|
|
168
|
+
for (const edge of outgoing.get(current.id) || []) {
|
|
169
|
+
if (allowed && !allowed.has(edge.id)) continue;
|
|
170
|
+
if (current.seen.has(edge.target)) continue;
|
|
171
|
+
const path = [...current.edges, edge];
|
|
172
|
+
if (edge.target === targetId) {
|
|
173
|
+
found.push(path);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
queue.push({ id: edge.target, edges: path, seen: new Set([...current.seen, edge.target]) });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return found.length === 1 ? found[0] : [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function inferVisitedEdges(model, runtime, outgoing) {
|
|
183
|
+
const ids = new Set([
|
|
184
|
+
...(runtime.visitedEdges || []),
|
|
185
|
+
...(runtime.edgeVisits || []).map((visit) => visit.edgeId),
|
|
186
|
+
]);
|
|
187
|
+
if (ids.size) return ids;
|
|
188
|
+
const ordered = [...runtime.activities]
|
|
189
|
+
.map((record, index) => ({ record, order: recordOrder(record, index) }))
|
|
190
|
+
.sort((a, b) => a.order - b.order)
|
|
191
|
+
.map(({ record }) => record.elementId)
|
|
192
|
+
.filter((id, index, values) => id && (!index || id !== values[index - 1]));
|
|
193
|
+
for (let index = 0; index < ordered.length - 1; index += 1) {
|
|
194
|
+
for (const edge of findPath(outgoing, ordered[index], ordered[index + 1], null, model.nodes.length + 1)) ids.add(edge.id);
|
|
195
|
+
}
|
|
196
|
+
return ids;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function latestRuntimeElement(runtime, nodeMap) {
|
|
200
|
+
return runtime.activities
|
|
201
|
+
.map((record, index) => ({ record, order: recordOrder(record, index) }))
|
|
202
|
+
.filter(({ record }) => nodeMap.has(record.elementId) && !isGateway(nodeMap.get(record.elementId)))
|
|
203
|
+
.sort((a, b) => a.order - b.order)
|
|
204
|
+
.at(-1)?.record?.elementId || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function predictForward({ model, runtime, presentation, maps, effectiveVisited, options }) {
|
|
208
|
+
const startId = latestRuntimeElement(runtime, maps.nodes);
|
|
209
|
+
const nodeIds = new Set();
|
|
210
|
+
const edgeIds = new Set();
|
|
211
|
+
const orderedNodeIds = [];
|
|
212
|
+
if (!startId || ['completed', 'terminated'].includes(runtime.status)) return { nodeIds, edgeIds, orderedNodeIds };
|
|
213
|
+
|
|
214
|
+
const walk = (nodeId, seen, depth = 0) => {
|
|
215
|
+
if (depth > model.nodes.length * 2 || seen.has(nodeId)) return;
|
|
216
|
+
const node = maps.nodes.get(nodeId);
|
|
217
|
+
const outgoing = maps.outgoing.get(nodeId) || [];
|
|
218
|
+
if (!outgoing.length) return;
|
|
219
|
+
let candidates = outgoing.filter((edge) => effectiveVisited.has(edge.id));
|
|
220
|
+
if (!candidates.length) {
|
|
221
|
+
if (outgoing.length === 1) candidates = outgoing;
|
|
222
|
+
else if (node?.type === 'parallelGateway') candidates = outgoing;
|
|
223
|
+
else if (CONDITIONAL_GATEWAYS.has(node?.type) || isGateway(node)) return;
|
|
224
|
+
else return;
|
|
225
|
+
}
|
|
226
|
+
for (const edge of candidates) {
|
|
227
|
+
edgeIds.add(edge.id);
|
|
228
|
+
const target = maps.nodes.get(edge.target);
|
|
229
|
+
if (!target) continue;
|
|
230
|
+
if (isVisibleTraceNode(target, { predicted: true, ...options }) && !nodeIds.has(target.id)) {
|
|
231
|
+
nodeIds.add(target.id);
|
|
232
|
+
orderedNodeIds.push(target.id);
|
|
233
|
+
}
|
|
234
|
+
walk(target.id, new Set([...seen, nodeId]), depth + 1);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
walk(startId, new Set());
|
|
238
|
+
return { nodeIds, edgeIds, orderedNodeIds };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function projectedEdgeId(sourceId, targetId, sourceEdges) {
|
|
242
|
+
const suffix = sourceEdges.map((edge) => edge.id).join('_').replace(/[^A-Za-z0-9_-]/g, '_');
|
|
243
|
+
return `Trace_${sourceId}_${targetId}_${suffix}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildProjectedGraph({ model, visibleIds, allowedEdgeIds, maps }) {
|
|
247
|
+
const graph = cloneModel(model);
|
|
248
|
+
graph.nodes = model.nodes.filter((node) => visibleIds.has(node.id)).map((node) => cloneModel(node));
|
|
249
|
+
graph.edges = [];
|
|
250
|
+
const keys = new Set();
|
|
251
|
+
for (const source of graph.nodes) {
|
|
252
|
+
const queue = (maps.outgoing.get(source.id) || [])
|
|
253
|
+
.filter((edge) => allowedEdgeIds.has(edge.id))
|
|
254
|
+
.map((edge) => ({ nodeId: edge.target, chain: [edge], seen: new Set([source.id]) }));
|
|
255
|
+
while (queue.length) {
|
|
256
|
+
const current = queue.shift();
|
|
257
|
+
if (current.seen.has(current.nodeId)) continue;
|
|
258
|
+
if (visibleIds.has(current.nodeId)) {
|
|
259
|
+
const key = `${source.id}>${current.nodeId}>${current.chain.map((edge) => edge.id).join(',')}`;
|
|
260
|
+
if (source.id !== current.nodeId && !keys.has(key)) {
|
|
261
|
+
keys.add(key);
|
|
262
|
+
const named = current.chain.find((edge) => edge.name);
|
|
263
|
+
graph.edges.push(createEdge(source.id, current.nodeId, {
|
|
264
|
+
id: projectedEdgeId(source.id, current.nodeId, current.chain),
|
|
265
|
+
name: named?.name || '',
|
|
266
|
+
condition: '',
|
|
267
|
+
sourceEdgeIds: current.chain.map((edge) => edge.id),
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
for (const edge of maps.outgoing.get(current.nodeId) || []) {
|
|
273
|
+
if (!allowedEdgeIds.has(edge.id)) continue;
|
|
274
|
+
queue.push({ nodeId: edge.target, chain: [...current.chain, edge], seen: new Set([...current.seen, current.nodeId]) });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
graph.settings = { ...(graph.settings || {}), direction: 'horizontal' };
|
|
279
|
+
if (graph.nodes.length > 1) beautify(graph, { direction: 'horizontal', density: 'balanced', edgeStyle: graph.settings.edgeStyle || 'rounded' });
|
|
280
|
+
return graph;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function projectRuntime(runtime, graphModel, presentation) {
|
|
284
|
+
const projectedVisited = [...runtime.visitedEdges];
|
|
285
|
+
const projectedEdgeVisits = [...runtime.edgeVisits];
|
|
286
|
+
for (const edge of graphModel.edges) {
|
|
287
|
+
const sources = edge.sourceEdgeIds || [];
|
|
288
|
+
if (!sources.length) continue;
|
|
289
|
+
const sourcePresentations = sources.map((id) => presentation.getEdge(id));
|
|
290
|
+
if (sourcePresentations.every((item) => item.historicallyVisited)) projectedVisited.push(edge.id);
|
|
291
|
+
const visits = sources.map((id) => runtime.edgeVisits
|
|
292
|
+
.filter((visit) => visit.edgeId === id)
|
|
293
|
+
.map((visit, index) => ({ visit, order: edgeVisitOrder(visit, index) }))
|
|
294
|
+
.sort((a, b) => a.order - b.order)
|
|
295
|
+
.at(-1)).filter(Boolean);
|
|
296
|
+
if (visits.length === sources.length) {
|
|
297
|
+
const latest = visits.sort((a, b) => a.order - b.order).at(-1);
|
|
298
|
+
projectedEdgeVisits.push({
|
|
299
|
+
...latest.visit,
|
|
300
|
+
id: `TraceVisit_${edge.id}`,
|
|
301
|
+
edgeId: edge.id,
|
|
302
|
+
status: sourcePresentations.some((item) => item.superseded) ? 'superseded' : 'effective',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const transitions = runtime.transitions.map((transition) => ({
|
|
307
|
+
...transition,
|
|
308
|
+
invalidatedEdgeIds: transition.invalidatedEdgeIds
|
|
309
|
+
? [
|
|
310
|
+
...transition.invalidatedEdgeIds,
|
|
311
|
+
...graphModel.edges.filter((edge) => edge.sourceEdgeIds?.some((id) => transition.invalidatedEdgeIds.includes(id))).map((edge) => edge.id),
|
|
312
|
+
]
|
|
313
|
+
: undefined,
|
|
314
|
+
}));
|
|
315
|
+
return { ...runtime, transitions, visitedEdges: [...new Set(projectedVisited)], edgeVisits: projectedEdgeVisits };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function buildVisitItems(model, runtime, presentation, predictedNodeIds, orderedPredictedIds, options) {
|
|
319
|
+
const nodeMap = new Map(model.nodes.map((node) => [node.id, node]));
|
|
320
|
+
const transitionActionIds = new Set(presentation.getTransitions().map((transition) => transition.actionId).filter(Boolean));
|
|
321
|
+
const groups = new Map();
|
|
322
|
+
runtime.activities.forEach((record, index) => {
|
|
323
|
+
const node = nodeMap.get(record.elementId);
|
|
324
|
+
if (!node || isGateway(node) || !isVisibleTraceNode(node, { actual: true, ...options })) return;
|
|
325
|
+
const visitKey = record.visitId || record.multiInstanceId || record.id || String(index);
|
|
326
|
+
const key = `${record.elementId}:${visitKey}`;
|
|
327
|
+
if (!groups.has(key)) groups.set(key, { key, node, visitId: visitKey, records: [], order: recordOrder(record, index), originalIndex: index });
|
|
328
|
+
const group = groups.get(key);
|
|
329
|
+
group.records.push(record);
|
|
330
|
+
group.order = Math.min(group.order, recordOrder(record, index));
|
|
331
|
+
});
|
|
332
|
+
const rounds = new Map();
|
|
333
|
+
const items = [...groups.values()].sort((a, b) => (a.order - b.order) || (a.originalIndex - b.originalIndex)).map((group) => {
|
|
334
|
+
const round = (rounds.get(group.node.id) || 0) + 1;
|
|
335
|
+
rounds.set(group.node.id, round);
|
|
336
|
+
const status = visitStatus(group.records);
|
|
337
|
+
const participants = uniqueParticipants(group.records);
|
|
338
|
+
const startTime = group.records.map((record) => record.startTime).filter(Boolean).sort()[0];
|
|
339
|
+
const endTime = group.records.map((record) => record.endTime).filter(Boolean).sort().at(-1);
|
|
340
|
+
const actions = (presentation.getNode(group.node.id).visits.find((visit) => visit.id === group.visitId)?.actions || [])
|
|
341
|
+
.filter((action) => !transitionActionIds.has(action.id));
|
|
342
|
+
const latestAction = [...actions].reverse().find((action) => action.plainText || action.assetCount) || actions.at(-1) || null;
|
|
343
|
+
const imageCount = actions.reduce((sum, action) => sum + action.imageCount, 0);
|
|
344
|
+
const fileCount = actions.reduce((sum, action) => sum + action.fileCount, 0);
|
|
345
|
+
const assetSummary = latestAction
|
|
346
|
+
? [latestAction.imageCount ? `${latestAction.imageCount} 张图片` : '', latestAction.fileCount ? `${latestAction.fileCount} 个附件` : ''].filter(Boolean).join(' · ')
|
|
347
|
+
: '';
|
|
348
|
+
return {
|
|
349
|
+
id: `TraceItem_${group.node.id}_${group.visitId}`,
|
|
350
|
+
kind: isStartOrEnd(group.node) ? 'milestone' : 'activity',
|
|
351
|
+
elementId: group.node.id,
|
|
352
|
+
visitId: group.visitId,
|
|
353
|
+
round,
|
|
354
|
+
name: group.node.name || NODE_DEFINITIONS[group.node.type]?.label || group.node.id,
|
|
355
|
+
nodeType: group.node.type,
|
|
356
|
+
automated: AUTOMATED_TASKS.has(group.node.type),
|
|
357
|
+
status,
|
|
358
|
+
statusLabel: statusLabel(status, group.records),
|
|
359
|
+
participants,
|
|
360
|
+
summary: participantSummary(participants) || presentation.getNode(group.node.id).summary,
|
|
361
|
+
records: group.records,
|
|
362
|
+
actions,
|
|
363
|
+
latestAction,
|
|
364
|
+
actionText: latestAction?.plainText || '',
|
|
365
|
+
actionSummary: latestAction ? [latestAction.label, latestAction.plainText, assetSummary].filter(Boolean).join(' · ') : '',
|
|
366
|
+
imageCount,
|
|
367
|
+
fileCount,
|
|
368
|
+
assetCount: imageCount + fileCount,
|
|
369
|
+
startTime,
|
|
370
|
+
endTime,
|
|
371
|
+
time: endTime || startTime || '',
|
|
372
|
+
outcome: group.records.map((record) => record.outcome).filter(Boolean).at(-1) || '',
|
|
373
|
+
comment: latestAction?.plainText || group.records.map((record) => record.comment).filter(Boolean).at(-1) || '',
|
|
374
|
+
predicted: false,
|
|
375
|
+
order: group.order,
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
let predictedOrder = Math.max(0, ...items.map((item) => item.order)) + 1;
|
|
379
|
+
for (const nodeId of orderedPredictedIds) {
|
|
380
|
+
if (!predictedNodeIds.has(nodeId) || items.some((item) => item.elementId === nodeId)) continue;
|
|
381
|
+
const node = nodeMap.get(nodeId);
|
|
382
|
+
if (!isVisibleTraceNode(node, { predicted: true, ...options })) continue;
|
|
383
|
+
items.push({
|
|
384
|
+
id: `TraceItem_Predicted_${node.id}`,
|
|
385
|
+
kind: isStartOrEnd(node) ? 'milestone' : 'activity',
|
|
386
|
+
elementId: node.id,
|
|
387
|
+
visitId: null,
|
|
388
|
+
round: 0,
|
|
389
|
+
name: node.name || NODE_DEFINITIONS[node.type]?.label || node.id,
|
|
390
|
+
nodeType: node.type,
|
|
391
|
+
automated: false,
|
|
392
|
+
status: 'idle',
|
|
393
|
+
statusLabel: '未到达',
|
|
394
|
+
participants: [],
|
|
395
|
+
summary: '待处理',
|
|
396
|
+
records: [],
|
|
397
|
+
actions: [],
|
|
398
|
+
latestAction: null,
|
|
399
|
+
actionText: '',
|
|
400
|
+
actionSummary: '',
|
|
401
|
+
imageCount: 0,
|
|
402
|
+
fileCount: 0,
|
|
403
|
+
assetCount: 0,
|
|
404
|
+
startTime: '',
|
|
405
|
+
endTime: '',
|
|
406
|
+
time: '',
|
|
407
|
+
outcome: '',
|
|
408
|
+
comment: '',
|
|
409
|
+
predicted: true,
|
|
410
|
+
order: predictedOrder++,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
for (const transition of presentation.getTransitions().filter((item) => ABNORMAL_TRANSITIONS.has(item.type))) {
|
|
414
|
+
items.push({
|
|
415
|
+
id: `TraceTransition_${transition.id}`,
|
|
416
|
+
kind: 'transition',
|
|
417
|
+
transitionId: transition.id,
|
|
418
|
+
type: transition.type,
|
|
419
|
+
name: transition.label,
|
|
420
|
+
status: transition.state,
|
|
421
|
+
statusLabel: transition.type === 'reject' ? '驳回' : '退回',
|
|
422
|
+
summary: transition.operator || '',
|
|
423
|
+
time: transition.occurredAt || transition.time || '',
|
|
424
|
+
comment: transition.action?.plainText || transition.comment || '',
|
|
425
|
+
actions: transition.action ? [transition.action] : [],
|
|
426
|
+
latestAction: transition.action || null,
|
|
427
|
+
actionText: transition.action?.plainText || '',
|
|
428
|
+
actionSummary: transition.action ? [transition.action.label, transition.action.plainText].filter(Boolean).join(' · ') : '',
|
|
429
|
+
imageCount: transition.action?.imageCount || 0,
|
|
430
|
+
fileCount: transition.action?.fileCount || 0,
|
|
431
|
+
assetCount: transition.action?.assetCount || 0,
|
|
432
|
+
sourceElementId: transition.sourceElementId,
|
|
433
|
+
targetElementId: transition.targetElementId,
|
|
434
|
+
predicted: false,
|
|
435
|
+
order: transitionOrder(transition, 0),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return items.sort((a, b) => (a.order - b.order) || a.id.localeCompare(b.id));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildTimelineLinks(items, maps, historicalEdgeIds, allowedEdgeIds) {
|
|
442
|
+
const links = [];
|
|
443
|
+
const traceActivities = items.filter((item) => item.kind !== 'transition');
|
|
444
|
+
for (let index = 0; index < traceActivities.length - 1; index += 1) {
|
|
445
|
+
const source = traceActivities[index];
|
|
446
|
+
const target = traceActivities[index + 1];
|
|
447
|
+
if (source.elementId === target.elementId) continue;
|
|
448
|
+
const allowed = source.predicted || target.predicted ? allowedEdgeIds : historicalEdgeIds;
|
|
449
|
+
const path = findPath(maps.outgoing, source.elementId, target.elementId, allowed, maps.nodes.size + 1);
|
|
450
|
+
if (!path.length) continue;
|
|
451
|
+
links.push({
|
|
452
|
+
id: `TraceLink_${source.id}_${target.id}`,
|
|
453
|
+
sourceItemId: source.id,
|
|
454
|
+
targetItemId: target.id,
|
|
455
|
+
sourceElementId: source.elementId,
|
|
456
|
+
targetElementId: target.elementId,
|
|
457
|
+
sourceEdgeIds: path.map((edge) => edge.id),
|
|
458
|
+
label: path.find((edge) => edge.name)?.name || '',
|
|
459
|
+
predicted: source.predicted || target.predicted,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return links;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function firstVisibleFrom(edge, maps, visibleElementIds, allowedEdgeIds) {
|
|
466
|
+
const queue = [edge.target];
|
|
467
|
+
const seen = new Set();
|
|
468
|
+
while (queue.length) {
|
|
469
|
+
const nodeId = queue.shift();
|
|
470
|
+
if (seen.has(nodeId)) continue;
|
|
471
|
+
seen.add(nodeId);
|
|
472
|
+
if (visibleElementIds.has(nodeId)) return nodeId;
|
|
473
|
+
for (const next of maps.outgoing.get(nodeId) || []) if (allowedEdgeIds.has(next.id)) queue.push(next.target);
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildParallelGroups(model, items, maps, allowedEdgeIds) {
|
|
479
|
+
const itemByElement = new Map();
|
|
480
|
+
for (const item of items.filter((entry) => entry.kind !== 'transition')) {
|
|
481
|
+
if (!itemByElement.has(item.elementId) || item.predicted) itemByElement.set(item.elementId, item);
|
|
482
|
+
}
|
|
483
|
+
const visibleElementIds = new Set(itemByElement.keys());
|
|
484
|
+
const groups = [];
|
|
485
|
+
for (const gateway of model.nodes.filter((node) => node.type === 'parallelGateway')) {
|
|
486
|
+
const outgoing = (maps.outgoing.get(gateway.id) || []).filter((edge) => allowedEdgeIds.has(edge.id));
|
|
487
|
+
if (outgoing.length < 2) continue;
|
|
488
|
+
const itemIds = [...new Set(outgoing.map((edge) => firstVisibleFrom(edge, maps, visibleElementIds, allowedEdgeIds)).filter(Boolean).map((id) => itemByElement.get(id)?.id).filter(Boolean))];
|
|
489
|
+
if (itemIds.length < 2) continue;
|
|
490
|
+
const children = itemIds.map((id) => items.find((item) => item.id === id));
|
|
491
|
+
groups.push({
|
|
492
|
+
id: `TraceGroup_${gateway.id}`,
|
|
493
|
+
kind: 'parallel',
|
|
494
|
+
gatewayId: gateway.id,
|
|
495
|
+
label: gateway.name || '并行审批',
|
|
496
|
+
itemIds,
|
|
497
|
+
completed: children.filter((item) => item.status === 'completed').length,
|
|
498
|
+
total: children.length,
|
|
499
|
+
status: children.some((item) => item.status === 'active') ? 'active' : children.every((item) => item.status === 'completed') ? 'completed' : 'idle',
|
|
500
|
+
order: Math.min(...children.map((item) => item.order)),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return groups;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function createRuntimeTraceProjection({ model, runtime, presentation, options = {} } = {}) {
|
|
507
|
+
if (!model) throw new Error('createRuntimeTraceProjection requires model.');
|
|
508
|
+
const rootModel = getScopeGraph(model, model.id);
|
|
509
|
+
const normalizedSource = normalizeRuntime(runtime);
|
|
510
|
+
const collapsed = collapseRuntimeToRoot(model, rootModel, normalizedSource);
|
|
511
|
+
const normalized = collapsed.runtime;
|
|
512
|
+
const hasCollapsedActivities = collapsed.activitySources.size > 0;
|
|
513
|
+
const runtimePresentation = !hasCollapsedActivities && presentation
|
|
514
|
+
? presentation
|
|
515
|
+
: createRuntimePresentation({ model: rootModel, runtime: normalized });
|
|
516
|
+
const maps = graphMaps(rootModel);
|
|
517
|
+
const historicalEdgeIds = inferVisitedEdges(rootModel, normalized, maps.outgoing);
|
|
518
|
+
const effectiveVisited = new Set([...historicalEdgeIds].filter((edgeId) => !runtimePresentation.getEdge(edgeId).superseded));
|
|
519
|
+
const prediction = options.includePredicted === false
|
|
520
|
+
? { nodeIds: new Set(), edgeIds: new Set(), orderedNodeIds: [] }
|
|
521
|
+
: predictForward({ model: rootModel, runtime: normalized, presentation: runtimePresentation, maps, effectiveVisited, options });
|
|
522
|
+
const allowedEdgeIds = new Set([...historicalEdgeIds, ...prediction.edgeIds]);
|
|
523
|
+
const actualNodeIds = new Set(normalized.activities.map((record) => record.elementId));
|
|
524
|
+
for (const edgeId of historicalEdgeIds) {
|
|
525
|
+
const edge = maps.edges.get(edgeId);
|
|
526
|
+
if (!edge) continue;
|
|
527
|
+
if (isStartOrEnd(maps.nodes.get(edge.source))) actualNodeIds.add(edge.source);
|
|
528
|
+
if (isStartOrEnd(maps.nodes.get(edge.target))) actualNodeIds.add(edge.target);
|
|
529
|
+
}
|
|
530
|
+
const visibleIds = new Set(rootModel.nodes.filter((node) => isVisibleTraceNode(node, {
|
|
531
|
+
actual: actualNodeIds.has(node.id),
|
|
532
|
+
predicted: prediction.nodeIds.has(node.id),
|
|
533
|
+
...options,
|
|
534
|
+
})).map((node) => node.id));
|
|
535
|
+
const graphModel = buildProjectedGraph({ model: rootModel, visibleIds, allowedEdgeIds, maps });
|
|
536
|
+
const graphRuntime = projectRuntime(normalized, graphModel, runtimePresentation);
|
|
537
|
+
const items = buildVisitItems(rootModel, normalized, runtimePresentation, prediction.nodeIds, prediction.orderedNodeIds, options);
|
|
538
|
+
const links = buildTimelineLinks(items, maps, historicalEdgeIds, allowedEdgeIds);
|
|
539
|
+
const groups = buildParallelGroups(rootModel, items, maps, allowedEdgeIds);
|
|
540
|
+
const warnings = [];
|
|
541
|
+
for (const record of normalizedSource.activities) {
|
|
542
|
+
if (!model.nodes.some((node) => node.id === record.elementId)) warnings.push(`运行记录引用了不存在的元素:${record.elementId}`);
|
|
543
|
+
}
|
|
544
|
+
const nodeMappings = Object.fromEntries(graphModel.nodes.map((node) => [
|
|
545
|
+
node.id,
|
|
546
|
+
[node.id, ...(collapsed.activitySources.get(node.id) || [])],
|
|
547
|
+
]));
|
|
548
|
+
const edgeMappings = Object.fromEntries(graphModel.edges.map((edge) => [edge.id, edge.sourceEdgeIds || [edge.id]]));
|
|
549
|
+
const itemMappings = Object.fromEntries(items.map((item) => [item.id, {
|
|
550
|
+
elementIds: item.kind === 'transition'
|
|
551
|
+
? [item.sourceElementId, item.targetElementId].filter(Boolean)
|
|
552
|
+
: [item.elementId, ...(collapsed.activitySources.get(item.elementId) || [])].filter(Boolean),
|
|
553
|
+
activityIds: (item.records || []).map((record) => record.id).filter(Boolean),
|
|
554
|
+
visitIds: [...new Set((item.records || []).map((record) => collapsed.activityVisitSources.get(record.id) || record.visitId).filter(Boolean))],
|
|
555
|
+
}]));
|
|
556
|
+
const edgeVisitMappings = Object.fromEntries(graphModel.edges.map((edge) => [
|
|
557
|
+
edge.id,
|
|
558
|
+
normalizedSource.edgeVisits.filter((visit) => (edge.sourceEdgeIds || [edge.id]).includes(visit.edgeId)).map((visit) => visit.id).filter(Boolean),
|
|
559
|
+
]));
|
|
560
|
+
return {
|
|
561
|
+
graphModel,
|
|
562
|
+
graphRuntime,
|
|
563
|
+
items,
|
|
564
|
+
links,
|
|
565
|
+
groups,
|
|
566
|
+
warnings,
|
|
567
|
+
mappings: {
|
|
568
|
+
nodes: nodeMappings,
|
|
569
|
+
edges: edgeMappings,
|
|
570
|
+
items: itemMappings,
|
|
571
|
+
edgeVisits: edgeVisitMappings,
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
}
|