@bpmn-nova/studio 0.3.4-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 -298
- package/dist/config.js +285 -0
- package/dist/controller.js +19 -4
- package/dist/index.d.ts +101 -2
- package/dist/modules/export-svg/render.js +84 -64
- package/dist/modules/node-geometry/index.d.ts +19 -0
- package/dist/modules/node-geometry/index.js +147 -0
- package/dist/modules/renderer-svg/index.js +64 -25
- 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.d.ts +1 -1
- package/dist/modules/viewer/index.js +220 -50
- package/dist/modules/viewer/runtime-trace.js +33 -30
- package/dist/modules/viewer/timeline.js +2 -2
- package/dist/panel-selection.js +60 -0
- package/dist/shell.js +315 -87
- package/dist/sidebars.js +195 -0
- package/dist/styles.css +72 -12
- package/llms-full.txt +3924 -826
- package/llms.txt +64 -15
- 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
|
+
}
|
|
@@ -248,7 +248,7 @@ export class BpmnViewer {
|
|
|
248
248
|
setModel(model: ProcessModel): void
|
|
249
249
|
setRuntime(runtime: ProcessInstanceSnapshot | null): void
|
|
250
250
|
refreshPresentation(): void
|
|
251
|
-
setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: RuntimeAssetResolver | null }): void
|
|
251
|
+
setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: RuntimeAssetResolver | null; replace?: boolean }): void
|
|
252
252
|
setProjection(projection: ViewerProjection): void
|
|
253
253
|
setTheme(theme: NovaThemeInput): NovaThemeState
|
|
254
254
|
setThemeMode(mode: NovaThemeMode): NovaThemeState
|
|
@@ -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
|
}
|
|
@@ -394,6 +400,12 @@ export class BpmnViewer {
|
|
|
394
400
|
this.projectedRuntime = null;
|
|
395
401
|
this.runtimePresentation = null;
|
|
396
402
|
this.selection = null;
|
|
403
|
+
this._sidebarTransitionActive = false;
|
|
404
|
+
this._sidebarViewport = null;
|
|
405
|
+
this._lastViewport = null;
|
|
406
|
+
this._timelineScroll = 0;
|
|
407
|
+
this._silentViewport = false;
|
|
408
|
+
this._pendingFit = false;
|
|
397
409
|
this._detailsCleanup = null;
|
|
398
410
|
this._detailsMotion = null;
|
|
399
411
|
this._detailsRoot = null;
|
|
@@ -425,9 +437,11 @@ export class BpmnViewer {
|
|
|
425
437
|
}
|
|
426
438
|
this._resizeObserver = typeof ResizeObserver === 'function'
|
|
427
439
|
? new ResizeObserver(() => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
440
|
+
this._refreshAutoProjection();
|
|
441
|
+
if (this._pendingFit && this._containerHasSize()) {
|
|
442
|
+
this._pendingFit = false;
|
|
443
|
+
this.renderer?.fitView?.();
|
|
444
|
+
}
|
|
431
445
|
})
|
|
432
446
|
: null;
|
|
433
447
|
this._resizeObserver?.observe(options.container);
|
|
@@ -436,10 +450,33 @@ export class BpmnViewer {
|
|
|
436
450
|
_resolveProjection() {
|
|
437
451
|
if (this.projection !== 'auto') return this.projection;
|
|
438
452
|
if (!this.runtime) return 'standard';
|
|
453
|
+
if (this._sidebarTransitionActive) return this.activeProjection;
|
|
439
454
|
const width = this.container.clientWidth || this.container.getBoundingClientRect?.().width || 0;
|
|
440
455
|
return width > 0 && width < 720 ? 'compact' : 'approval';
|
|
441
456
|
}
|
|
442
457
|
|
|
458
|
+
// The Shell owns the transition. Do not replace the renderer at intermediate widths.
|
|
459
|
+
_setSidebarTransitionActive(active) {
|
|
460
|
+
if (this._sidebarTransitionActive === Boolean(active)) return;
|
|
461
|
+
this._sidebarTransitionActive = Boolean(active);
|
|
462
|
+
if (active) {
|
|
463
|
+
if (this.activeProjection !== 'compact') this._sidebarViewport = this.renderer?.getViewportState?.() || this._sidebarViewport;
|
|
464
|
+
} else this._refreshAutoProjection();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_refreshAutoProjection() {
|
|
468
|
+
if (this._sidebarTransitionActive || this.projection !== 'auto') return;
|
|
469
|
+
const next = this._resolveProjection();
|
|
470
|
+
if (next === this.activeProjection) return;
|
|
471
|
+
if (this.activeProjection !== 'compact') this._sidebarViewport = this.renderer?.getViewportState?.() || 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
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
443
480
|
_rebuildProjection() {
|
|
444
481
|
const previousProjection = this.activeProjection;
|
|
445
482
|
this.activeProjection = this._resolveProjection();
|
|
@@ -494,33 +531,59 @@ export class BpmnViewer {
|
|
|
494
531
|
return [];
|
|
495
532
|
}
|
|
496
533
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
534
|
+
_resolveTraceClick(payload) {
|
|
535
|
+
if (!payload) return null;
|
|
536
|
+
const targetType = payload.targetType;
|
|
537
|
+
const transitionId = payload.transition?.id || payload.traceItem?.transitionId;
|
|
538
|
+
const transition = targetType === 'transition' && transitionId
|
|
539
|
+
? this.runtimePresentation.getTransition(transitionId)
|
|
540
|
+
: null;
|
|
541
|
+
if (targetType === 'transition' && !transition) return null;
|
|
542
|
+
const elementId = transition?.sourceElementId || payload.elementId || payload.element?.id || payload.traceItem?.elementId;
|
|
543
|
+
let selectedElement;
|
|
544
|
+
if (targetType === 'edge') {
|
|
545
|
+
const current = this.projectedModel.edges.find((edge) => edge.id === elementId)
|
|
546
|
+
|| this.model.edges.find((edge) => edge.id === elementId);
|
|
547
|
+
if (!current) return null;
|
|
548
|
+
const sourceIds = current.sourceEdgeIds?.length ? current.sourceEdgeIds : [current.id];
|
|
501
549
|
const sourceEdges = sourceIds.map((id) => this.model.edges.find((edge) => edge.id === id)).filter(Boolean);
|
|
502
|
-
|
|
503
|
-
} else
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
550
|
+
selectedElement = sourceEdges.find((edge) => current.name && edge.name === current.name) || sourceEdges[0] || current;
|
|
551
|
+
} else selectedElement = this.model.nodes.find((node) => node.id === elementId) || null;
|
|
552
|
+
if (!selectedElement && !transition) return null;
|
|
553
|
+
const presentation = selectedElement?.id && targetType !== 'edge'
|
|
554
|
+
? this.runtimePresentation.getNode(selectedElement.id)
|
|
555
|
+
: null;
|
|
556
|
+
const visitId = payload.visitId ?? payload.traceItem?.visitId ?? null;
|
|
557
|
+
const traceItems = this.traceProjection?.items || [];
|
|
558
|
+
const traceItem = traceItems.find((item) => payload.traceItem?.id && item.id === payload.traceItem.id)
|
|
559
|
+
|| (targetType === 'transition' ? traceItems.find((item) => item.transitionId === transitionId) : null)
|
|
560
|
+
|| (visitId ? traceItems.find((item) => item.elementId === elementId && item.visitId === visitId) : null)
|
|
561
|
+
|| null;
|
|
562
|
+
const visit = visitId ? presentation?.visits?.find((item) => item.id === visitId) : null;
|
|
563
|
+
if (targetType === 'visit' && visitId && !traceItem && !visit) return null;
|
|
564
|
+
const records = traceItem?.records || visit?.records;
|
|
565
|
+
return {
|
|
566
|
+
targetType,
|
|
567
|
+
elementId: selectedElement?.id || elementId,
|
|
568
|
+
element: selectedElement,
|
|
569
|
+
visitId,
|
|
570
|
+
traceItem,
|
|
571
|
+
transition,
|
|
516
572
|
presentation,
|
|
517
|
-
activityInstances: this._activityInstancesFor({
|
|
518
|
-
actions:
|
|
573
|
+
activityInstances: records ? [...records] : this._activityInstancesFor({ presentation }),
|
|
574
|
+
actions: traceItem?.actions || visit?.actions || (transition?.action ? [transition.action] : presentation?.actions) || [],
|
|
519
575
|
runtime: this.runtime,
|
|
520
576
|
projection: this.activeProjection,
|
|
521
577
|
originalEvent: payload.originalEvent,
|
|
522
578
|
viewer: this,
|
|
523
|
-
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
_emitTraceClick(payload) {
|
|
583
|
+
const resolved = this._resolveTraceClick(payload);
|
|
584
|
+
if (!resolved) return;
|
|
585
|
+
this.options._onPanelSelection?.(resolved);
|
|
586
|
+
this.onTraceClick(resolved);
|
|
524
587
|
}
|
|
525
588
|
|
|
526
589
|
_mountRenderer() {
|
|
@@ -554,7 +617,7 @@ export class BpmnViewer {
|
|
|
554
617
|
onDetailsRequest: (item, anchor, event) => {
|
|
555
618
|
const node = this.model.nodes.find((candidate) => candidate.id === item.elementId);
|
|
556
619
|
if (!node) return;
|
|
557
|
-
this._select('node', node, event, { emitTrace: false });
|
|
620
|
+
this._select('node', node, event, { emitTrace: false, traceItem: item });
|
|
558
621
|
const presentation = this.runtimePresentation.getNode(node.id);
|
|
559
622
|
if (presentation.hasDetails && this.runtimeDetailsOptions.autoOpen) {
|
|
560
623
|
this._openRuntimeDetails(node, presentation, anchor, {
|
|
@@ -564,6 +627,7 @@ export class BpmnViewer {
|
|
|
564
627
|
},
|
|
565
628
|
onTransitionRequest: (item, anchor, event) => {
|
|
566
629
|
const transition = this.runtimePresentation.getTransition(item.transitionId);
|
|
630
|
+
this.options._onPanelSelection?.(this._resolveTraceClick({ targetType: 'transition', transition, traceItem: item, originalEvent: event }));
|
|
567
631
|
if (this.runtimeDetailsOptions.autoOpen) {
|
|
568
632
|
this._openRuntimeTransitionDetails(transition, anchor, {
|
|
569
633
|
restoreFocusVisible: event ? event.detail === 0 : Boolean(anchor?.matches?.(':focus-visible')),
|
|
@@ -598,21 +662,27 @@ export class BpmnViewer {
|
|
|
598
662
|
this._emitTraceClick({ targetType: 'transition', element, transition, originalEvent: event });
|
|
599
663
|
if (this.runtimeDetailsOptions.autoOpen) this.openRuntimeTransitionDetails(transition, event.currentTarget);
|
|
600
664
|
},
|
|
601
|
-
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
|
+
},
|
|
602
670
|
});
|
|
603
671
|
}
|
|
604
672
|
|
|
605
|
-
_select(kind, element, originalEvent = undefined, { emitTrace = true } = {}) {
|
|
673
|
+
_select(kind, element, originalEvent = undefined, { emitTrace = true, traceItem = null } = {}) {
|
|
606
674
|
this.selection = { kind, id: element.id };
|
|
607
675
|
this.renderer.setSelection(this.selection);
|
|
608
676
|
const presentation = kind === 'node' ? this.runtimePresentation.getNode(element.id) : null;
|
|
609
677
|
this.onElementClick({ kind, element, runtime: this.runtime, presentation });
|
|
610
678
|
if (emitTrace) this._emitTraceClick({ targetType: kind, element, presentation, originalEvent });
|
|
679
|
+
else this.options._onPanelSelection?.(this._resolveTraceClick({ targetType: traceItem ? 'visit' : kind, element, presentation, traceItem, originalEvent }));
|
|
611
680
|
}
|
|
612
681
|
|
|
613
682
|
clearSelection() {
|
|
614
683
|
this.selection = null;
|
|
615
684
|
this.renderer.setSelection(null);
|
|
685
|
+
this.options._onPanelSelection?.(null);
|
|
616
686
|
this.onElementClick(null);
|
|
617
687
|
}
|
|
618
688
|
|
|
@@ -918,46 +988,144 @@ export class BpmnViewer {
|
|
|
918
988
|
});
|
|
919
989
|
}
|
|
920
990
|
|
|
921
|
-
|
|
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();
|
|
922
1063
|
this._disposeRuntimeDetails();
|
|
923
1064
|
this._rebuildProjection();
|
|
924
|
-
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();
|
|
925
1071
|
if (this.selection) this.renderer.setSelection?.(this.selection);
|
|
1072
|
+
else this.renderer.setSelection?.(null);
|
|
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);
|
|
926
1082
|
}
|
|
927
1083
|
|
|
928
|
-
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
|
+
}
|
|
929
1090
|
setRuntime(runtime) {
|
|
930
|
-
|
|
1091
|
+
const next = runtime ? normalizeRuntime(runtime) : null;
|
|
1092
|
+
const sameInstance = Boolean(this.runtime) && Boolean(next) && (this.runtime.processInstanceId || '') === (next.processInstanceId || '');
|
|
1093
|
+
this.runtime = next;
|
|
931
1094
|
this.mode = this.options.mode === 'instance' || this.runtime ? 'instance' : 'viewer';
|
|
932
1095
|
this._svgExportAssetCache.clear();
|
|
933
|
-
this.refresh();
|
|
1096
|
+
this.refresh({ preserveViewport: sameInstance });
|
|
934
1097
|
}
|
|
935
1098
|
setProjection(projection) {
|
|
936
1099
|
if (!['auto', 'standard', 'approval', 'compact'].includes(projection)) return;
|
|
937
1100
|
this.projection = projection;
|
|
938
|
-
this.refresh();
|
|
939
|
-
requestAnimationFrame(() =>
|
|
1101
|
+
this.refresh({ preserveViewport: false });
|
|
1102
|
+
requestAnimationFrame(() => {
|
|
1103
|
+
if (this._containerHasSize()) this.renderer?.fitView?.();
|
|
1104
|
+
else this._pendingFit = true;
|
|
1105
|
+
});
|
|
940
1106
|
}
|
|
941
|
-
setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver } = {}) {
|
|
1107
|
+
setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver, replace = false } = {}) {
|
|
942
1108
|
let changed = false;
|
|
943
1109
|
if (runtimeAssetResolver !== undefined) {
|
|
944
1110
|
this.runtimeAssetResolver = runtimeAssetResolver || null;
|
|
945
1111
|
this._svgExportAssetCache.clear();
|
|
946
1112
|
changed = true;
|
|
947
1113
|
}
|
|
948
|
-
if (runtimeTraceOptions !== undefined) {
|
|
949
|
-
this.runtimeTraceOptions =
|
|
1114
|
+
if (replace || runtimeTraceOptions !== undefined) {
|
|
1115
|
+
this.runtimeTraceOptions = replace
|
|
1116
|
+
? { ...(runtimeTraceOptions || {}) }
|
|
1117
|
+
: { ...this.runtimeTraceOptions, ...(runtimeTraceOptions || {}) };
|
|
950
1118
|
changed = true;
|
|
951
1119
|
}
|
|
952
|
-
if (timeline !== undefined) {
|
|
953
|
-
this.timelineOptions = mergeTimelineOptions(this.timelineOptions, timeline || {});
|
|
1120
|
+
if (replace || timeline !== undefined) {
|
|
1121
|
+
this.timelineOptions = mergeTimelineOptions(replace ? {} : this.timelineOptions, timeline || {});
|
|
954
1122
|
changed = true;
|
|
955
1123
|
}
|
|
956
|
-
if (runtimeDetails !== undefined) {
|
|
957
|
-
this.runtimeDetailsOptions = mergeRuntimeDetailsOptions(this.runtimeDetailsOptions, runtimeDetails || {});
|
|
1124
|
+
if (replace || runtimeDetails !== undefined) {
|
|
1125
|
+
this.runtimeDetailsOptions = mergeRuntimeDetailsOptions(replace ? {} : this.runtimeDetailsOptions, runtimeDetails || {});
|
|
958
1126
|
changed = true;
|
|
959
1127
|
}
|
|
960
|
-
if (changed) this.refresh();
|
|
1128
|
+
if (changed) this.refresh({ preserveViewport: true });
|
|
961
1129
|
}
|
|
962
1130
|
setTheme(theme) { return this.themeController.setTheme(theme); }
|
|
963
1131
|
setThemeMode(mode) { return this.themeController.setMode(mode); }
|
|
@@ -965,7 +1133,7 @@ export class BpmnViewer {
|
|
|
965
1133
|
setRuntimeAppearance(runtimeAppearance) {
|
|
966
1134
|
this.runtimeAppearanceOptions = runtimeAppearance || {};
|
|
967
1135
|
this.runtimeAppearance = createRuntimeAppearance(this.runtimeAppearanceOptions);
|
|
968
|
-
this.refresh();
|
|
1136
|
+
this.refresh({ preserveViewport: true });
|
|
969
1137
|
}
|
|
970
1138
|
refreshPresentation() {
|
|
971
1139
|
if (this.mode !== 'viewer' || (this.runtime && this.activeProjection === 'compact')) return;
|
|
@@ -1013,6 +1181,8 @@ export class BpmnViewer {
|
|
|
1013
1181
|
fitView() { this.renderer.fitView(); }
|
|
1014
1182
|
zoomBy(delta) { this.renderer.zoomBy(delta); }
|
|
1015
1183
|
destroy() {
|
|
1184
|
+
this._sidebarTransitionActive = false;
|
|
1185
|
+
this._sidebarViewport = null;
|
|
1016
1186
|
this._svgExportPreview?.close?.({ immediate: true });
|
|
1017
1187
|
this._svgExportPreview = null;
|
|
1018
1188
|
this._svgExportAssetCache.clear();
|