@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.
@@ -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, fallback = 0) {
17
- if (!value) return fallback;
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, index = 0) {
23
- return timestamp(record.startTime || record.endTime, index);
20
+ function recordOrder(record) {
21
+ return timestamp(record.startTime || record.endTime);
24
22
  }
25
23
 
26
- function transitionOrder(transition, index = 0) {
27
- return timestamp(transition.occurredAt || transition.time, index);
24
+ function transitionOrder(transition) {
25
+ return timestamp(transition.occurredAt || transition.time);
28
26
  }
29
27
 
30
- function edgeVisitOrder(visit, index = 0) {
31
- return timestamp(visit.occurredAt || visit.time, index);
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((a, b) => a.order - b.order)
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((a, b) => a.order - b.order)
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((a, b) => a.order - b.order)
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((a, b) => a.order - b.order).at(-1);
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, index), originalIndex: index });
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
- group.order = Math.min(group.order, recordOrder(record, index));
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((a, b) => (a.order - b.order) || (a.originalIndex - b.originalIndex)).map((group) => {
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 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;
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, assetSummary].filter(Boolean).join(' · ') : '',
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((a, b) => (a.order - b.order) || a.id.localeCompare(b.id));
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
- if (!value) return '';
35
- return String(value).replace('T', ' ');
35
+ return formatRuntimeInstant(value);
36
36
  }
37
37
 
38
38
  function renderParticipants(item) {
@@ -0,0 +1,60 @@
1
+ // Snapshot only public data. Never freeze the Controller, Viewer, or their source models.
2
+ function immutableCopy(value, seen = new WeakMap()) {
3
+ if (!value || typeof value !== 'object') return value;
4
+ if (seen.has(value)) return seen.get(value);
5
+ const copy = Array.isArray(value) ? [] : {};
6
+ seen.set(value, copy);
7
+ for (const key of Object.keys(value)) {
8
+ Object.defineProperty(copy, key, {
9
+ value: immutableCopy(value[key], seen),
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ });
14
+ }
15
+ return Object.freeze(copy);
16
+ }
17
+
18
+ const EMPTY_SELECTION = Object.freeze({ selection: null, selectedElement: null, trace: null });
19
+
20
+ /** Resolve current facts rather than retaining a clicked node or an old Runtime snapshot. */
21
+ export function createPanelSelection(studio, viewer, previousTrace = null) {
22
+ if (!viewer) {
23
+ const selection = studio.selection;
24
+ const selectedElement = studio.getSelectedElement();
25
+ if (!selection || !selectedElement) return EMPTY_SELECTION;
26
+ return immutableCopy({ selection, selectedElement, trace: null });
27
+ }
28
+
29
+ // An explicit clear must stay cleared across refreshes, even when the Viewer still
30
+ // has an older visual selection (for example after a selected visit disappears).
31
+ if (!previousTrace) return EMPTY_SELECTION;
32
+ const current = viewer._resolveTraceClick(previousTrace);
33
+ if (!current) return EMPTY_SELECTION;
34
+ const { viewer: liveViewer, originalEvent: _originalEvent, ...traceData } = current;
35
+ const selection = current.element
36
+ ? { kind: current.targetType === 'edge' ? 'edge' : 'node', id: current.element.id }
37
+ : null;
38
+ const snapshot = immutableCopy({ selection, selectedElement: current.element || null, trace: traceData });
39
+ return Object.freeze({
40
+ ...snapshot,
41
+ trace: Object.freeze({ ...snapshot.trace, viewer: liveViewer }),
42
+ });
43
+ }
44
+
45
+ /** Ignore fresh object identity so refreshes and paired timeline callbacks notify only once. */
46
+ export function samePanelSelection(previous, next) {
47
+ const pairs = new WeakMap();
48
+ const same = (left, right) => {
49
+ if (Object.is(left, right)) return true;
50
+ if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
51
+ if (pairs.has(left)) return pairs.get(left) === right;
52
+ pairs.set(left, right);
53
+ const keys = Object.keys(left);
54
+ if (keys.length !== Object.keys(right).length) return false;
55
+ return keys.every((key) => Object.hasOwn(right, key) && (key === 'viewer'
56
+ ? left[key] === right[key]
57
+ : same(left[key], right[key])));
58
+ };
59
+ return same(previous, next);
60
+ }