@bpmn-nova/studio 0.3.5-preview → 0.3.6-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 +16 -367
- package/dist/modules/export-svg/render.js +2 -2
- package/dist/modules/renderer-svg/index.js +1 -0
- package/dist/modules/runtime/diagnostics.js +22 -0
- package/dist/modules/runtime/index.d.ts +38 -4
- package/dist/modules/runtime/index.js +272 -80
- package/dist/modules/runtime/time.js +41 -0
- package/dist/modules/runtime/visits.js +167 -0
- package/dist/modules/viewer/index.js +139 -20
- package/dist/modules/viewer/runtime-trace.js +33 -30
- package/dist/modules/viewer/timeline.js +2 -2
- package/dist/shell.js +1 -1
- package/llms-full.txt +3394 -1749
- package/llms.txt +6 -4
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { parseRuntimeInstant, compareRuntimeOrder, runtimeInstant } from './time.js';
|
|
2
|
+
import { pushDiagnostic } from './diagnostics.js';
|
|
3
|
+
|
|
4
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled', 'skipped']);
|
|
5
|
+
|
|
6
|
+
export function resolveVisitStatus(records = []) {
|
|
7
|
+
if (!records.length) return 'idle';
|
|
8
|
+
if (records.some((item) => item.status === 'failed')) return 'failed';
|
|
9
|
+
if (records.some((item) => item.status === 'active')) return 'active';
|
|
10
|
+
if (records.every((item) => item.status === 'skipped')) return 'skipped';
|
|
11
|
+
if (records.some((item) => item.status === 'cancelled') && !records.some((item) => item.status === 'completed')) return 'cancelled';
|
|
12
|
+
return 'completed';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveEffectiveStatus(visits = []) {
|
|
16
|
+
const records = visits.flatMap((visit) => visit.records || []);
|
|
17
|
+
if (!records.length) return 'idle';
|
|
18
|
+
if (records.some((item) => item.status === 'active')) return 'active';
|
|
19
|
+
if (records.some((item) => item.status === 'failed')) return 'failed';
|
|
20
|
+
if (records.some((item) => item.status === 'completed')) return 'completed';
|
|
21
|
+
if (records.some((item) => item.status === 'cancelled') && !records.some((item) => item.status === 'completed')) return 'cancelled';
|
|
22
|
+
if (records.every((item) => item.status === 'skipped')) return 'skipped';
|
|
23
|
+
return resolveVisitStatus(records);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function groupVisits(records = []) {
|
|
27
|
+
const groups = new Map();
|
|
28
|
+
records.forEach((record, index) => {
|
|
29
|
+
const key = record.visitId
|
|
30
|
+
? `visit:${record.visitId}`
|
|
31
|
+
: record.multiInstanceId
|
|
32
|
+
? `multi:${record.multiInstanceId}`
|
|
33
|
+
: `record:${record.id || index}`;
|
|
34
|
+
if (!groups.has(key)) {
|
|
35
|
+
groups.set(key, {
|
|
36
|
+
id: record.visitId || record.multiInstanceId || record.id || String(index),
|
|
37
|
+
records: [],
|
|
38
|
+
index,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
groups.get(key).records.push(record);
|
|
42
|
+
});
|
|
43
|
+
return [...groups.values()]
|
|
44
|
+
.sort((left, right) => compareRuntimeOrder(visitOrderKey(left), visitOrderKey(right)))
|
|
45
|
+
.map((visit, index) => ({ id: visit.id, records: visit.records, round: index + 1, index: visit.index }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function markEffectiveVisits(visits = [], { elementId, transitions = [], diagnostics = null } = {}) {
|
|
49
|
+
if (visits.length <= 1) {
|
|
50
|
+
return visits.map((visit) => ({ ...visit, effective: true, superseded: false }));
|
|
51
|
+
}
|
|
52
|
+
const ranges = visits.map((visit, index) => ({ visit, index, ...visitRange(visit) }));
|
|
53
|
+
const superseded = new Set();
|
|
54
|
+
|
|
55
|
+
for (const visit of visits) {
|
|
56
|
+
for (const record of visit.records) {
|
|
57
|
+
const target = record.supersedesVisitId;
|
|
58
|
+
if (!target) continue;
|
|
59
|
+
if (visits.some((candidate) => candidate.id === target)) superseded.add(target);
|
|
60
|
+
else {
|
|
61
|
+
pushDiagnostic(diagnostics, {
|
|
62
|
+
code: 'unknown-superseded-visit',
|
|
63
|
+
level: 'warning',
|
|
64
|
+
message: 'supersedesVisitId 未匹配到同节点 Visit。',
|
|
65
|
+
fieldPath: `activities.supersedesVisitId`,
|
|
66
|
+
elementId,
|
|
67
|
+
activityId: record.id,
|
|
68
|
+
visitId: visit.id,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const inbound = transitions.filter((transition) => ['reject', 'return'].includes(transition.type) && transition.targetElementId === elementId);
|
|
75
|
+
const timed = ranges.some((range) => range.start != null || range.end != null);
|
|
76
|
+
|
|
77
|
+
for (let earlierIndex = 0; earlierIndex < ranges.length; earlierIndex += 1) {
|
|
78
|
+
for (let laterIndex = earlierIndex + 1; laterIndex < ranges.length; laterIndex += 1) {
|
|
79
|
+
const earlier = ranges[earlierIndex];
|
|
80
|
+
const later = ranges[laterIndex];
|
|
81
|
+
if (superseded.has(later.visit.id)) continue;
|
|
82
|
+
const sequential = isSequentialReplacement(earlier, later, inbound);
|
|
83
|
+
const overlapping = isOverlappingOpen(earlier, later);
|
|
84
|
+
if (overlapping && !sequential) {
|
|
85
|
+
pushDiagnostic(diagnostics, {
|
|
86
|
+
code: 'concurrent-visits',
|
|
87
|
+
level: 'info',
|
|
88
|
+
message: '同一节点存在时间重叠且未声明替代关系的独立 Visit。',
|
|
89
|
+
fieldPath: 'activities.visitId',
|
|
90
|
+
elementId,
|
|
91
|
+
visitId: earlier.visit.id,
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (sequential) superseded.add(earlier.visit.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!timed) {
|
|
100
|
+
const activeCount = ranges.filter((range) => range.status === 'active').length;
|
|
101
|
+
if (activeCount <= 1) {
|
|
102
|
+
visits.slice(0, -1).forEach((visit) => superseded.add(visit.id));
|
|
103
|
+
} else {
|
|
104
|
+
pushDiagnostic(diagnostics, {
|
|
105
|
+
code: 'ambiguous-visit-order',
|
|
106
|
+
level: 'warning',
|
|
107
|
+
message: '多个 Visit 缺少可比较时间,未将历史 active 解释为先后重办。',
|
|
108
|
+
fieldPath: 'activities.startTime',
|
|
109
|
+
elementId,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return visits.map((visit) => {
|
|
115
|
+
const isSuperseded = superseded.has(visit.id);
|
|
116
|
+
return { ...visit, effective: !isSuperseded, superseded: isSuperseded };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function visitOrderKey(visit) {
|
|
121
|
+
let instant = null;
|
|
122
|
+
visit.records.forEach((record) => {
|
|
123
|
+
const start = runtimeInstant(record.startTime);
|
|
124
|
+
const end = runtimeInstant(record.endTime);
|
|
125
|
+
const candidate = start ?? end;
|
|
126
|
+
if (candidate == null) return;
|
|
127
|
+
instant = instant == null ? candidate : Math.min(instant, candidate);
|
|
128
|
+
});
|
|
129
|
+
return { instant, index: visit.index || 0 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function visitRange(visit) {
|
|
133
|
+
let start = null;
|
|
134
|
+
let end = null;
|
|
135
|
+
let open = false;
|
|
136
|
+
for (const record of visit.records) {
|
|
137
|
+
const started = parseRuntimeInstant(record.startTime).instant;
|
|
138
|
+
const ended = parseRuntimeInstant(record.endTime).instant;
|
|
139
|
+
if (started != null) start = start == null ? started : Math.min(start, started);
|
|
140
|
+
if (ended != null) end = end == null ? ended : Math.max(end, ended);
|
|
141
|
+
if (record.status === 'active' && ended == null) open = true;
|
|
142
|
+
}
|
|
143
|
+
const status = resolveVisitStatus(visit.records);
|
|
144
|
+
if (status === 'active') open = true;
|
|
145
|
+
return { start, end, open, status };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isOverlappingOpen(earlier, later) {
|
|
149
|
+
if (!earlier.open) return false;
|
|
150
|
+
if (later.start == null) return earlier.open && later.open;
|
|
151
|
+
if (earlier.end == null) return true;
|
|
152
|
+
return later.start < earlier.end;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isSequentialReplacement(earlier, later, inbound) {
|
|
156
|
+
if (!TERMINAL.has(earlier.status) && !earlier.open) return false;
|
|
157
|
+
if (earlier.open && !TERMINAL.has(earlier.status)) return false;
|
|
158
|
+
if (earlier.end != null && later.start != null && later.start >= earlier.end) return true;
|
|
159
|
+
if (TERMINAL.has(earlier.status) && !earlier.open && later.start != null && earlier.start != null && later.start > earlier.start) return true;
|
|
160
|
+
const laterInstant = later.start ?? later.end;
|
|
161
|
+
return inbound.some((transition) => {
|
|
162
|
+
const at = runtimeInstant(transition.occurredAt || transition.time);
|
|
163
|
+
if (at == null || laterInstant == null) return inbound.length > 0 && TERMINAL.has(earlier.status);
|
|
164
|
+
const afterEarlier = earlier.end == null || at >= earlier.end || (earlier.start != null && at >= earlier.start);
|
|
165
|
+
return afterEarlier && at <= laterInstant;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cloneModel, createEdge, getScopeGraph, NODE_DEFINITIONS } from '../core/index.js';
|
|
2
2
|
import { createDefaultIconRegistry, createIconElement } from '../icons/index.js';
|
|
3
3
|
import { DiagramRenderer } from '../renderer-svg/index.js';
|
|
4
|
-
import { createRuntimePresentation, normalizeRuntime } from '../runtime/index.js';
|
|
4
|
+
import { compareRuntimeOrder, createRuntimePresentation, formatRuntimeInstant, normalizeRuntime, parseRuntimeInstant } from '../runtime/index.js';
|
|
5
5
|
import { createRuntimeTraceProjection } from './runtime-trace.js';
|
|
6
6
|
import { renderDefaultRuntimeTimeline, RuntimeTimelineHost } from './timeline.js';
|
|
7
7
|
import { renderRuntimeApprovalContent } from './runtime-content.js';
|
|
@@ -51,7 +51,7 @@ function renderRuntimeAction({ container, viewer, action, resolveAsset, compact
|
|
|
51
51
|
element('em', '', appearance.label),
|
|
52
52
|
);
|
|
53
53
|
header.appendChild(heading);
|
|
54
|
-
if (action.occurredAt) header.appendChild(element('time', '', action.occurredAt));
|
|
54
|
+
if (action.occurredAt) header.appendChild(element('time', '', formatRuntimeInstant(action.occurredAt)));
|
|
55
55
|
article.appendChild(header);
|
|
56
56
|
if (action.targets?.length) article.appendChild(element('div', 'mb-runtime-action-targets', `目标:${action.targets.map((target) => target.name).join('、')}`));
|
|
57
57
|
if (action.targetElementId && !action.targets?.length) article.appendChild(element('div', 'mb-runtime-action-targets', `目标节点:${action.targetElementId}`));
|
|
@@ -104,7 +104,7 @@ export function renderDefaultRuntimeDetails({ container, viewer, node, presentat
|
|
|
104
104
|
applyRuntimeTone(recordStatus, viewer?.runtimeAppearance?.resolveStatus(record.status));
|
|
105
105
|
title.append(element('strong', '', record.participant?.name || record.assignee || '待分配审批人'), recordStatus);
|
|
106
106
|
copy.appendChild(title);
|
|
107
|
-
const time = [record.startTime, record.endTime].filter(Boolean).join(' — ');
|
|
107
|
+
const time = [record.startTime, record.endTime].map((value) => formatRuntimeInstant(value)).filter(Boolean).join(' — ');
|
|
108
108
|
if (time) copy.appendChild(element('time', '', time));
|
|
109
109
|
row.append(avatar, copy);
|
|
110
110
|
body.appendChild(row);
|
|
@@ -113,7 +113,10 @@ export function renderDefaultRuntimeDetails({ container, viewer, node, presentat
|
|
|
113
113
|
const renderedActionIds = new Set();
|
|
114
114
|
if (!visits.length) body.appendChild(element('div', 'mb-runtime-details-empty', presentation.summary || presentation.statusLabel));
|
|
115
115
|
visits.forEach((visit) => {
|
|
116
|
-
if (presentation.visits.length > 1)
|
|
116
|
+
if (presentation.visits.length > 1) {
|
|
117
|
+
const historical = visit.effective === false || visit.superseded;
|
|
118
|
+
body.appendChild(element('div', 'mb-runtime-details-round', historical ? `第 ${visit.round} 次处理 · 历史` : `第 ${visit.round} 次处理`));
|
|
119
|
+
}
|
|
117
120
|
[...visit.records].reverse().forEach(appendRecord);
|
|
118
121
|
[...visit.actions].reverse().forEach((action) => {
|
|
119
122
|
renderRuntimeAction({ container: body, viewer, action, resolveAsset });
|
|
@@ -130,7 +133,7 @@ export function renderDefaultRuntimeDetails({ container, viewer, node, presentat
|
|
|
130
133
|
const block = element('article', `mb-runtime-details-transition type-${transition.type}`);
|
|
131
134
|
applyRuntimeTone(block, viewer?.runtimeAppearance?.resolveTransition(transition).tone);
|
|
132
135
|
block.appendChild(element('strong', '', transition.type === 'reject' ? `驳回至 ${transition.targetName || transition.targetElementId}` : `退回至 ${transition.targetName || transition.targetElementId}`));
|
|
133
|
-
const meta = [transition.operator, transition.occurredAt || transition.time].filter(Boolean).join(' · ');
|
|
136
|
+
const meta = [transition.operator, formatRuntimeInstant(transition.occurredAt || transition.time)].filter(Boolean).join(' · ');
|
|
134
137
|
if (meta) block.appendChild(element('span', '', meta));
|
|
135
138
|
if (transition.action && !renderedActionIds.has(transition.action.id)) {
|
|
136
139
|
renderRuntimeApprovalContent({
|
|
@@ -189,7 +192,7 @@ export function renderDefaultRuntimeTransitionDetails({
|
|
|
189
192
|
actorStatus,
|
|
190
193
|
);
|
|
191
194
|
actorCopy.appendChild(actorTitle);
|
|
192
|
-
const time = transition.action?.occurredAt || transition.occurredAt || transition.time;
|
|
195
|
+
const time = formatRuntimeInstant(transition.action?.occurredAt || transition.occurredAt || transition.time);
|
|
193
196
|
if (time) actorCopy.appendChild(element('time', '', time));
|
|
194
197
|
actor.append(avatar, actorCopy);
|
|
195
198
|
body.appendChild(actor);
|
|
@@ -312,7 +315,10 @@ function projectRuntime(runtime, projectedModel) {
|
|
|
312
315
|
.map((id) => normalized.edgeVisits?.filter((visit) => visit.edgeId === id).at(-1))
|
|
313
316
|
.filter(Boolean);
|
|
314
317
|
if (visits.length === edge.sourceEdgeIds.length) {
|
|
315
|
-
const latest = visits
|
|
318
|
+
const latest = visits
|
|
319
|
+
.map((visit, index) => ({ visit, index, instant: parseRuntimeInstant(visit.occurredAt || visit.time).instant }))
|
|
320
|
+
.sort((left, right) => compareRuntimeOrder(left, right))
|
|
321
|
+
.at(-1)?.visit;
|
|
316
322
|
projectedEdgeVisits.push({ ...latest, id: `ProjectedVisit_${edge.id}`, edgeId: edge.id });
|
|
317
323
|
}
|
|
318
324
|
}
|
|
@@ -396,6 +402,10 @@ export class BpmnViewer {
|
|
|
396
402
|
this.selection = null;
|
|
397
403
|
this._sidebarTransitionActive = false;
|
|
398
404
|
this._sidebarViewport = null;
|
|
405
|
+
this._lastViewport = null;
|
|
406
|
+
this._timelineScroll = 0;
|
|
407
|
+
this._silentViewport = false;
|
|
408
|
+
this._pendingFit = false;
|
|
399
409
|
this._detailsCleanup = null;
|
|
400
410
|
this._detailsMotion = null;
|
|
401
411
|
this._detailsRoot = null;
|
|
@@ -426,7 +436,13 @@ export class BpmnViewer {
|
|
|
426
436
|
throw error;
|
|
427
437
|
}
|
|
428
438
|
this._resizeObserver = typeof ResizeObserver === 'function'
|
|
429
|
-
? new ResizeObserver(() =>
|
|
439
|
+
? new ResizeObserver(() => {
|
|
440
|
+
this._refreshAutoProjection();
|
|
441
|
+
if (this._pendingFit && this._containerHasSize()) {
|
|
442
|
+
this._pendingFit = false;
|
|
443
|
+
this.renderer?.fitView?.();
|
|
444
|
+
}
|
|
445
|
+
})
|
|
430
446
|
: null;
|
|
431
447
|
this._resizeObserver?.observe(options.container);
|
|
432
448
|
}
|
|
@@ -453,8 +469,12 @@ export class BpmnViewer {
|
|
|
453
469
|
const next = this._resolveProjection();
|
|
454
470
|
if (next === this.activeProjection) return;
|
|
455
471
|
if (this.activeProjection !== 'compact') this._sidebarViewport = this.renderer?.getViewportState?.() || this._sidebarViewport;
|
|
456
|
-
this.refresh();
|
|
457
|
-
if (this.activeProjection !== 'compact' && this._sidebarViewport)
|
|
472
|
+
this.refresh({ preserveViewport: this.activeProjection !== 'compact' });
|
|
473
|
+
if (this.activeProjection !== 'compact' && this._sidebarViewport) {
|
|
474
|
+
this._silentViewport = true;
|
|
475
|
+
try { this.renderer?.setViewportState?.(this._sidebarViewport); }
|
|
476
|
+
finally { this._silentViewport = false; }
|
|
477
|
+
}
|
|
458
478
|
}
|
|
459
479
|
|
|
460
480
|
_rebuildProjection() {
|
|
@@ -642,7 +662,11 @@ export class BpmnViewer {
|
|
|
642
662
|
this._emitTraceClick({ targetType: 'transition', element, transition, originalEvent: event });
|
|
643
663
|
if (this.runtimeDetailsOptions.autoOpen) this.openRuntimeTransitionDetails(transition, event.currentTarget);
|
|
644
664
|
},
|
|
645
|
-
onViewportChange: (viewport) => {
|
|
665
|
+
onViewportChange: (viewport) => {
|
|
666
|
+
if (viewport) this._lastViewport = { zoom: viewport.zoom, pan: { ...viewport.pan } };
|
|
667
|
+
if (!this._silentViewport) this._disposeRuntimeDetails();
|
|
668
|
+
this.onViewportChange(viewport);
|
|
669
|
+
},
|
|
646
670
|
});
|
|
647
671
|
}
|
|
648
672
|
|
|
@@ -964,26 +988,121 @@ export class BpmnViewer {
|
|
|
964
988
|
});
|
|
965
989
|
}
|
|
966
990
|
|
|
967
|
-
|
|
991
|
+
_rendererKind() {
|
|
992
|
+
if (!this.renderer) return null;
|
|
993
|
+
return this.renderer instanceof RuntimeTimelineHost ? 'timeline' : 'diagram';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
_containerHasSize() {
|
|
997
|
+
if (typeof document !== 'undefined' && document.hidden) return false;
|
|
998
|
+
const width = this.container.clientWidth || this.container.getBoundingClientRect?.().width || 0;
|
|
999
|
+
const height = this.container.clientHeight || this.container.getBoundingClientRect?.().height || 0;
|
|
1000
|
+
return width >= 1 && height >= 1;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
_capturePresentationState() {
|
|
1004
|
+
const kind = this._rendererKind();
|
|
1005
|
+
if (kind === 'diagram') {
|
|
1006
|
+
const viewport = this.renderer?.getViewportState?.();
|
|
1007
|
+
if (viewport) this._lastViewport = viewport;
|
|
1008
|
+
} else if (kind === 'timeline') {
|
|
1009
|
+
this._timelineScroll = this.container.scrollTop || this._timelineScroll || 0;
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
kind,
|
|
1013
|
+
details: this._detailsTarget ? { ...this._detailsTarget, anchor: this._detailsAnchor } : null,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
_syncDiagramRenderer() {
|
|
1018
|
+
this.renderer.options.visualModel = this.model;
|
|
1019
|
+
this.renderer.options.runtimePresenter = () => this.runtimePresentation;
|
|
1020
|
+
this.renderer.options.runtimeAppearance = this.runtimeAppearance;
|
|
1021
|
+
this.renderer.mode = this.mode;
|
|
1022
|
+
this.renderer.model = this.projectedModel;
|
|
1023
|
+
this.renderer.runtime = this.projectedRuntime;
|
|
1024
|
+
this.renderer.render();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
_syncTimelineRenderer() {
|
|
1028
|
+
Object.assign(this.renderer.options, {
|
|
1029
|
+
model: this.projectedModel,
|
|
1030
|
+
runtime: this.projectedRuntime,
|
|
1031
|
+
presentation: this.runtimePresentation,
|
|
1032
|
+
projection: this.traceProjection,
|
|
1033
|
+
...this._resolveTimelineOptions(),
|
|
1034
|
+
appearance: this.runtimeAppearance,
|
|
1035
|
+
themeState: this.themeController.getState(),
|
|
1036
|
+
});
|
|
1037
|
+
this.renderer.render();
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
_pruneSelection() {
|
|
1041
|
+
if (!this.selection) return;
|
|
1042
|
+
const collection = this.selection.kind === 'edge' ? this.projectedModel?.edges : this.projectedModel?.nodes;
|
|
1043
|
+
if (!collection?.some((item) => item.id === this.selection.id)) this.selection = null;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
_restoreRuntimeDetails(target) {
|
|
1047
|
+
if (!target) return;
|
|
1048
|
+
if (target.kind === 'node') {
|
|
1049
|
+
const node = this.model.nodes.find((item) => item.id === target.id);
|
|
1050
|
+
const presentation = node ? this.runtimePresentation.getNode(node.id) : null;
|
|
1051
|
+
if (node && presentation?.hasDetails) this.openRuntimeDetails(node, presentation, target.anchor || null);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (target.kind === 'transition') {
|
|
1055
|
+
const transition = this.runtimePresentation.getTransition(target.id);
|
|
1056
|
+
if (transition) this._openRuntimeTransitionDetails(transition, target.anchor || null);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
refresh(options = {}) {
|
|
1061
|
+
const preserveViewport = options.preserveViewport === true;
|
|
1062
|
+
const previous = this._capturePresentationState();
|
|
968
1063
|
this._disposeRuntimeDetails();
|
|
969
1064
|
this._rebuildProjection();
|
|
970
|
-
this.
|
|
1065
|
+
const kind = this.runtime && this.activeProjection === 'compact' && this.traceProjection ? 'timeline' : 'diagram';
|
|
1066
|
+
const sameKind = previous.kind === kind && this.renderer;
|
|
1067
|
+
if (sameKind && kind === 'diagram') this._syncDiagramRenderer();
|
|
1068
|
+
else if (sameKind && kind === 'timeline') this._syncTimelineRenderer();
|
|
1069
|
+
else this._mountRenderer();
|
|
1070
|
+
this._pruneSelection();
|
|
971
1071
|
if (this.selection) this.renderer.setSelection?.(this.selection);
|
|
1072
|
+
else this.renderer.setSelection?.(null);
|
|
972
1073
|
this.options._onPanelSelection?.();
|
|
1074
|
+
this._silentViewport = true;
|
|
1075
|
+
try {
|
|
1076
|
+
if (kind === 'diagram' && preserveViewport && this._lastViewport) this.renderer.setViewportState?.(this._lastViewport);
|
|
1077
|
+
if (kind === 'timeline' && preserveViewport) this.container.scrollTop = this._timelineScroll || 0;
|
|
1078
|
+
} finally {
|
|
1079
|
+
this._silentViewport = false;
|
|
1080
|
+
}
|
|
1081
|
+
this._restoreRuntimeDetails(previous.details);
|
|
973
1082
|
}
|
|
974
1083
|
|
|
975
|
-
setModel(model) {
|
|
1084
|
+
setModel(model) {
|
|
1085
|
+
const same = this.model === model || Boolean(this.model?.id && model?.id && this.model.id === model.id);
|
|
1086
|
+
this.model = model;
|
|
1087
|
+
this._svgExportAssetCache.clear();
|
|
1088
|
+
this.refresh({ preserveViewport: same });
|
|
1089
|
+
}
|
|
976
1090
|
setRuntime(runtime) {
|
|
977
|
-
|
|
1091
|
+
const next = runtime ? normalizeRuntime(runtime) : null;
|
|
1092
|
+
const sameInstance = Boolean(this.runtime) && Boolean(next) && (this.runtime.processInstanceId || '') === (next.processInstanceId || '');
|
|
1093
|
+
this.runtime = next;
|
|
978
1094
|
this.mode = this.options.mode === 'instance' || this.runtime ? 'instance' : 'viewer';
|
|
979
1095
|
this._svgExportAssetCache.clear();
|
|
980
|
-
this.refresh();
|
|
1096
|
+
this.refresh({ preserveViewport: sameInstance });
|
|
981
1097
|
}
|
|
982
1098
|
setProjection(projection) {
|
|
983
1099
|
if (!['auto', 'standard', 'approval', 'compact'].includes(projection)) return;
|
|
984
1100
|
this.projection = projection;
|
|
985
|
-
this.refresh();
|
|
986
|
-
requestAnimationFrame(() =>
|
|
1101
|
+
this.refresh({ preserveViewport: false });
|
|
1102
|
+
requestAnimationFrame(() => {
|
|
1103
|
+
if (this._containerHasSize()) this.renderer?.fitView?.();
|
|
1104
|
+
else this._pendingFit = true;
|
|
1105
|
+
});
|
|
987
1106
|
}
|
|
988
1107
|
setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver, replace = false } = {}) {
|
|
989
1108
|
let changed = false;
|
|
@@ -1006,7 +1125,7 @@ export class BpmnViewer {
|
|
|
1006
1125
|
this.runtimeDetailsOptions = mergeRuntimeDetailsOptions(replace ? {} : this.runtimeDetailsOptions, runtimeDetails || {});
|
|
1007
1126
|
changed = true;
|
|
1008
1127
|
}
|
|
1009
|
-
if (changed) this.refresh();
|
|
1128
|
+
if (changed) this.refresh({ preserveViewport: true });
|
|
1010
1129
|
}
|
|
1011
1130
|
setTheme(theme) { return this.themeController.setTheme(theme); }
|
|
1012
1131
|
setThemeMode(mode) { return this.themeController.setMode(mode); }
|
|
@@ -1014,7 +1133,7 @@ export class BpmnViewer {
|
|
|
1014
1133
|
setRuntimeAppearance(runtimeAppearance) {
|
|
1015
1134
|
this.runtimeAppearanceOptions = runtimeAppearance || {};
|
|
1016
1135
|
this.runtimeAppearance = createRuntimeAppearance(this.runtimeAppearanceOptions);
|
|
1017
|
-
this.refresh();
|
|
1136
|
+
this.refresh({ preserveViewport: true });
|
|
1018
1137
|
}
|
|
1019
1138
|
refreshPresentation() {
|
|
1020
1139
|
if (this.mode !== 'viewer' || (this.runtime && this.activeProjection === 'compact')) return;
|
|
@@ -6,29 +6,34 @@ import {
|
|
|
6
6
|
getScopeGraph,
|
|
7
7
|
NODE_DEFINITIONS,
|
|
8
8
|
} from '../core/index.js';
|
|
9
|
-
import { createRuntimePresentation, normalizeRuntime } from '../runtime/index.js';
|
|
9
|
+
import { compareRuntimeOrder, createRuntimePresentation, normalizeRuntime, parseRuntimeInstant } from '../runtime/index.js';
|
|
10
10
|
|
|
11
11
|
const HUMAN_TASKS = new Set(['userTask', 'manualTask', 'callActivity']);
|
|
12
12
|
const AUTOMATED_TASKS = new Set(['serviceTask', 'scriptTask', 'businessRuleTask', 'sendTask', 'receiveTask']);
|
|
13
13
|
const CONDITIONAL_GATEWAYS = new Set(['exclusiveGateway', 'inclusiveGateway', 'eventBasedGateway', 'complexGateway']);
|
|
14
14
|
const ABNORMAL_TRANSITIONS = new Set(['reject', 'return']);
|
|
15
15
|
|
|
16
|
-
function timestamp(value
|
|
17
|
-
|
|
18
|
-
const parsed = Date.parse(String(value).replace(' ', 'T'));
|
|
19
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
16
|
+
function timestamp(value) {
|
|
17
|
+
return parseRuntimeInstant(value).instant;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
function recordOrder(record
|
|
23
|
-
return timestamp(record.startTime || record.endTime
|
|
20
|
+
function recordOrder(record) {
|
|
21
|
+
return timestamp(record.startTime || record.endTime);
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
function transitionOrder(transition
|
|
27
|
-
return timestamp(transition.occurredAt || transition.time
|
|
24
|
+
function transitionOrder(transition) {
|
|
25
|
+
return timestamp(transition.occurredAt || transition.time);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
function edgeVisitOrder(visit
|
|
31
|
-
return timestamp(visit.occurredAt || visit.time
|
|
28
|
+
function edgeVisitOrder(visit) {
|
|
29
|
+
return timestamp(visit.occurredAt || visit.time);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function byTime(left, right) {
|
|
33
|
+
return compareRuntimeOrder(
|
|
34
|
+
{ instant: left.order ?? left.instant ?? null, index: left.index ?? left.originalIndex ?? 0 },
|
|
35
|
+
{ instant: right.order ?? right.instant ?? null, index: right.index ?? right.originalIndex ?? 0 },
|
|
36
|
+
);
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
function visitStatus(records) {
|
|
@@ -186,8 +191,8 @@ function inferVisitedEdges(model, runtime, outgoing) {
|
|
|
186
191
|
]);
|
|
187
192
|
if (ids.size) return ids;
|
|
188
193
|
const ordered = [...runtime.activities]
|
|
189
|
-
.map((record, index) => ({ record, order: recordOrder(record, index
|
|
190
|
-
.sort(
|
|
194
|
+
.map((record, index) => ({ record, order: recordOrder(record), index }))
|
|
195
|
+
.sort(byTime)
|
|
191
196
|
.map(({ record }) => record.elementId)
|
|
192
197
|
.filter((id, index, values) => id && (!index || id !== values[index - 1]));
|
|
193
198
|
for (let index = 0; index < ordered.length - 1; index += 1) {
|
|
@@ -198,9 +203,9 @@ function inferVisitedEdges(model, runtime, outgoing) {
|
|
|
198
203
|
|
|
199
204
|
function latestRuntimeElement(runtime, nodeMap) {
|
|
200
205
|
return runtime.activities
|
|
201
|
-
.map((record, index) => ({ record, order: recordOrder(record, index
|
|
206
|
+
.map((record, index) => ({ record, order: recordOrder(record), index }))
|
|
202
207
|
.filter(({ record }) => nodeMap.has(record.elementId) && !isGateway(nodeMap.get(record.elementId)))
|
|
203
|
-
.sort(
|
|
208
|
+
.sort(byTime)
|
|
204
209
|
.at(-1)?.record?.elementId || null;
|
|
205
210
|
}
|
|
206
211
|
|
|
@@ -290,11 +295,11 @@ function projectRuntime(runtime, graphModel, presentation) {
|
|
|
290
295
|
if (sourcePresentations.every((item) => item.historicallyVisited)) projectedVisited.push(edge.id);
|
|
291
296
|
const visits = sources.map((id) => runtime.edgeVisits
|
|
292
297
|
.filter((visit) => visit.edgeId === id)
|
|
293
|
-
.map((visit, index) => ({ visit, order: edgeVisitOrder(visit, index
|
|
294
|
-
.sort(
|
|
298
|
+
.map((visit, index) => ({ visit, order: edgeVisitOrder(visit), index }))
|
|
299
|
+
.sort(byTime)
|
|
295
300
|
.at(-1)).filter(Boolean);
|
|
296
301
|
if (visits.length === sources.length) {
|
|
297
|
-
const latest = visits.sort(
|
|
302
|
+
const latest = visits.sort(byTime).at(-1);
|
|
298
303
|
projectedEdgeVisits.push({
|
|
299
304
|
...latest.visit,
|
|
300
305
|
id: `TraceVisit_${edge.id}`,
|
|
@@ -324,27 +329,25 @@ function buildVisitItems(model, runtime, presentation, predictedNodeIds, ordered
|
|
|
324
329
|
if (!node || isGateway(node) || !isVisibleTraceNode(node, { actual: true, ...options })) return;
|
|
325
330
|
const visitKey = record.visitId || record.multiInstanceId || record.id || String(index);
|
|
326
331
|
const key = `${record.elementId}:${visitKey}`;
|
|
327
|
-
if (!groups.has(key)) groups.set(key, { key, node, visitId: visitKey, records: [], order: recordOrder(record
|
|
332
|
+
if (!groups.has(key)) groups.set(key, { key, node, visitId: visitKey, records: [], order: recordOrder(record), originalIndex: index });
|
|
328
333
|
const group = groups.get(key);
|
|
329
334
|
group.records.push(record);
|
|
330
|
-
|
|
335
|
+
const nextOrder = recordOrder(record);
|
|
336
|
+
if (nextOrder != null) group.order = group.order == null ? nextOrder : Math.min(group.order, nextOrder);
|
|
331
337
|
});
|
|
332
338
|
const rounds = new Map();
|
|
333
|
-
const items = [...groups.values()].sort(
|
|
339
|
+
const items = [...groups.values()].sort(byTime).map((group) => {
|
|
334
340
|
const round = (rounds.get(group.node.id) || 0) + 1;
|
|
335
341
|
rounds.set(group.node.id, round);
|
|
336
342
|
const status = visitStatus(group.records);
|
|
337
343
|
const participants = uniqueParticipants(group.records);
|
|
338
344
|
const startTime = group.records.map((record) => record.startTime).filter(Boolean).sort()[0];
|
|
339
345
|
const endTime = group.records.map((record) => record.endTime).filter(Boolean).sort().at(-1);
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
const latestAction =
|
|
346
|
+
const visitPresentation = presentation.getNode(group.node.id).visits.find((visit) => visit.id === group.visitId);
|
|
347
|
+
const actions = (visitPresentation?.actions || []).filter((action) => !transitionActionIds.has(action.id));
|
|
348
|
+
const latestAction = visitPresentation?.latestAction || actions.at(-1) || null;
|
|
343
349
|
const imageCount = actions.reduce((sum, action) => sum + action.imageCount, 0);
|
|
344
350
|
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
351
|
return {
|
|
349
352
|
id: `TraceItem_${group.node.id}_${group.visitId}`,
|
|
350
353
|
kind: isStartOrEnd(group.node) ? 'milestone' : 'activity',
|
|
@@ -362,7 +365,7 @@ function buildVisitItems(model, runtime, presentation, predictedNodeIds, ordered
|
|
|
362
365
|
actions,
|
|
363
366
|
latestAction,
|
|
364
367
|
actionText: latestAction?.plainText || '',
|
|
365
|
-
actionSummary: latestAction ? [latestAction.label, latestAction.plainText
|
|
368
|
+
actionSummary: visitPresentation?.actionSummary || (latestAction ? [latestAction.label, latestAction.plainText].filter(Boolean).join(' · ') : ''),
|
|
366
369
|
imageCount,
|
|
367
370
|
fileCount,
|
|
368
371
|
assetCount: imageCount + fileCount,
|
|
@@ -375,7 +378,7 @@ function buildVisitItems(model, runtime, presentation, predictedNodeIds, ordered
|
|
|
375
378
|
order: group.order,
|
|
376
379
|
};
|
|
377
380
|
});
|
|
378
|
-
let predictedOrder = Math.max(0, ...items.map((item) => item.order)) + 1;
|
|
381
|
+
let predictedOrder = Math.max(0, ...items.map((item) => item.order).filter((value) => Number.isFinite(value))) + 1;
|
|
379
382
|
for (const nodeId of orderedPredictedIds) {
|
|
380
383
|
if (!predictedNodeIds.has(nodeId) || items.some((item) => item.elementId === nodeId)) continue;
|
|
381
384
|
const node = nodeMap.get(nodeId);
|
|
@@ -435,7 +438,7 @@ function buildVisitItems(model, runtime, presentation, predictedNodeIds, ordered
|
|
|
435
438
|
order: transitionOrder(transition, 0),
|
|
436
439
|
});
|
|
437
440
|
}
|
|
438
|
-
return items.sort((
|
|
441
|
+
return items.sort((left, right) => byTime(left, right) || left.id.localeCompare(right.id));
|
|
439
442
|
}
|
|
440
443
|
|
|
441
444
|
function buildTimelineLinks(items, maps, historicalEdgeIds, allowedEdgeIds) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createDefaultIconRegistry, createIconElement } from '../icons/index.js';
|
|
2
|
+
import { formatRuntimeInstant } from '../runtime/index.js';
|
|
2
3
|
import { renderRuntimeApprovalContent } from './runtime-content.js';
|
|
3
4
|
import { applyRuntimeTone } from '../theme/index.js';
|
|
4
5
|
|
|
@@ -31,8 +32,7 @@ function resultText(item) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function formatTime(value) {
|
|
34
|
-
|
|
35
|
-
return String(value).replace('T', ' ');
|
|
35
|
+
return formatRuntimeInstant(value);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function renderParticipants(item) {
|
package/dist/shell.js
CHANGED
|
@@ -1034,7 +1034,7 @@ export class BpmnStudioShell {
|
|
|
1034
1034
|
ui: Object.freeze({ ...this._config.ui, regions: Object.freeze(normalizeStudioRegions(regions, this._config.ui.regions)) }),
|
|
1035
1035
|
});
|
|
1036
1036
|
}
|
|
1037
|
-
if (changed || regions?.right === 'default') this._applyRegions();
|
|
1037
|
+
if (changed || regions?.right === 'default') this._applyRegions({ fit: false });
|
|
1038
1038
|
return this.getRegions();
|
|
1039
1039
|
}
|
|
1040
1040
|
_isSidebarHidden(side) {
|