From bcf46a97c97654d39f16fc659e5409671166fe81 Mon Sep 17 00:00:00 2001 From: Nour Assy <67027776+assynour@users.noreply.github.com> Date: Tue, 14 Mar 2023 14:42:07 +0100 Subject: [PATCH] feat: add resource allocation interaction (1st step) (#10) When clicking on the "Allocate more resource" button, the sub-process diagram is displayed. - It's running task is highlighted and the path leading to it is modified as well. This task proposed a popover which opens on hover. - In the future, the style of some elements of the diagram will be modified when hovering on the row of the popover. When clicking on the "Contact-Client" button, a window alert is displayed. In the future, it will display the 2nd process of the diagram and it will let interact with it. ### Implementation notes This PR introduces refactoring to better encapsulate the logic. In particular, it provides classes that may be reused outside this demo and served as starting point for experimental features - BpmnElementsSearcher - PathResolver The case monitoring data are fully managed by dedicated classes both for the main process and the sub-process. Other parts of the code are still leaking the use cases at several places. This will be refactored later to limit the size of this PR. --------- Co-authored-by: Thomas Bouffard <27200110+tbouffard@users.noreply.github.com> --- src/bpmn-elements.ts | 16 --- src/case-monitoring-data.ts | 134 ++++++++++++++++++ src/case-monitoring.ts | 266 ++++++++++++++++++++++-------------- src/diagram.ts | 11 +- src/style.css | 96 +++++-------- src/use-case-management.ts | 9 +- src/utils/bpmn-elements.ts | 76 +++++++++++ src/utils/paths.ts | 53 +++++++ 8 files changed, 476 insertions(+), 185 deletions(-) create mode 100644 src/case-monitoring-data.ts create mode 100644 src/utils/bpmn-elements.ts create mode 100644 src/utils/paths.ts diff --git a/src/bpmn-elements.ts b/src/bpmn-elements.ts index ee4f1c0..723deb0 100644 --- a/src/bpmn-elements.ts +++ b/src/bpmn-elements.ts @@ -33,19 +33,3 @@ export function isGateway(elementId: string): boolean { export function isEvent(elementId: string): boolean { return events.has(elementId); } - -export function getElementIdByName(elementName: string): string | undefined { - for (const [key, value] of activities.entries()) { - if (value === elementName) { - return key; - } - } - - for (const [key, value] of events.entries()) { - if (value === elementName) { - return key; - } - } - - return undefined; -} diff --git a/src/case-monitoring-data.ts b/src/case-monitoring-data.ts new file mode 100644 index 0000000..d9d0b28 --- /dev/null +++ b/src/case-monitoring-data.ts @@ -0,0 +1,134 @@ +import {type BpmnVisualization} from 'bpmn-visualization'; +import {BpmnElementsSearcher} from './utils/bpmn-elements.js'; +import {PathResolver} from './utils/paths.js'; + +export type CaseMonitoringData = { + executedShapes: string[]; + runningActivities: string[]; + enabledShapes: string[]; + pendingShapes: string[]; + visitedEdges: string[]; +}; + +/** + * Simulate fetching data from an execution system. + */ +abstract class AbstractCaseMonitoringDataProvider { + protected readonly bpmnElementsSearcher: BpmnElementsSearcher; + private readonly pathResolver: PathResolver; + + protected constructor(protected readonly bpmnVisualization: BpmnVisualization) { + this.bpmnElementsSearcher = new BpmnElementsSearcher(bpmnVisualization); + this.pathResolver = new PathResolver(bpmnVisualization); + } + + /** + * In real life, parameters would be passed to this method, at least the `case id`. + */ + fetch(): CaseMonitoringData { + const executedShapes = this.getExecutedShapes(); + const runningActivities = this.getRunningActivities(); + const enabledShapes = this.getEnabledShapes(); + const pendingShapes = this.getPendingShapes(); + + const visitedEdges = this.pathResolver.getVisitedEdges([...executedShapes, ...runningActivities, ...enabledShapes, ...pendingShapes]); + return { + executedShapes, + runningActivities, + enabledShapes, + pendingShapes, + visitedEdges, + }; + } + + abstract getExecutedShapes(): string[]; + + abstract getRunningActivities(): string[]; + + abstract getEnabledShapes(): string[]; + + abstract getPendingShapes(): string[]; +} + +class MainProcessCaseMonitoringDataProvider extends AbstractCaseMonitoringDataProvider { + constructor(protected readonly bpmnVisualization: BpmnVisualization) { + super(bpmnVisualization); + } + + getEnabledShapes(): string[] { + return []; + } + + getExecutedShapes(): string[] { + const shapes: string[] = []; + addNonNullElement(shapes, this.bpmnElementsSearcher.getElementIdByName('New POI needed')); // Start event + addNonNullElement(shapes, 'Gateway_0xh0plz'); // Parallel gateway after start event + addNonNullElement(shapes, this.bpmnElementsSearcher.getElementIdByName('Vendor creates invoice')); + addNonNullElement(shapes, this.bpmnElementsSearcher.getElementIdByName('Create Purchase Order Item')); + return shapes; + } + + getPendingShapes(): string[] { + const pendingShapes: string[] = []; + pendingShapes.push('Gateway_0domayw'); + return pendingShapes; + } + + getRunningActivities(): string[] { + const activities: string[] = []; + addNonNullElement(activities, this.bpmnElementsSearcher.getElementIdByName('SRM subprocess')); + return activities; + } +} + +class SecondaryProcessCaseMonitoringDataProvider extends AbstractCaseMonitoringDataProvider { + constructor(protected readonly bpmnVisualization: BpmnVisualization) { + super(bpmnVisualization); + } + + getEnabledShapes(): string[] { + const shapes: string[] = []; + addNonNullElement(shapes, this.bpmnElementsSearcher.getElementIdByName('SRM: Awaiting Approval')); + return shapes; + } + + getExecutedShapes(): string[] { + const shapes: string[] = []; + addNonNullElement(shapes, 'Event_1dnxra5'); // Start event + addNonNullElement(shapes, this.bpmnElementsSearcher.getElementIdByName('SRM: Created')); + addNonNullElement(shapes, this.bpmnElementsSearcher.getElementIdByName('SRM: Complete')); + return shapes; + } + + getPendingShapes(): string[] { + return []; + } + + getRunningActivities(): string[] { + return []; + } +} + +export function getCaseMonitoringData(processId: string, bpmnVisualization: BpmnVisualization): CaseMonitoringData { + const caseMonitoringDataProvider = processId === 'main' ? new MainProcessCaseMonitoringDataProvider(bpmnVisualization) : new SecondaryProcessCaseMonitoringDataProvider(bpmnVisualization); + + const executedShapes = caseMonitoringDataProvider.getExecutedShapes(); + const runningActivities = caseMonitoringDataProvider.getRunningActivities(); + const enabledShapes = caseMonitoringDataProvider.getEnabledShapes(); + const pendingShapes = caseMonitoringDataProvider.getPendingShapes(); + + const visitedEdges = new PathResolver(bpmnVisualization).getVisitedEdges([...executedShapes, ...runningActivities, ...enabledShapes, ...pendingShapes]); + return { + executedShapes, + runningActivities, + enabledShapes, + pendingShapes, + visitedEdges, + }; +} + +function addNonNullElement(elements: string[], elt: string | undefined) { + if (elt) { + elements.push(elt); + } +} diff --git a/src/case-monitoring.ts b/src/case-monitoring.ts index f27f723..8f810f0 100644 --- a/src/case-monitoring.ts +++ b/src/case-monitoring.ts @@ -1,82 +1,66 @@ import tippy, {type Instance, type Props, type ReferenceElement} from 'tippy.js'; import 'tippy.js/dist/tippy.css'; -import type {BpmnElement, BpmnSemantic, BpmnVisualization, EdgeBpmnSemantic, ShapeBpmnSemantic} from 'bpmn-visualization'; -import {getElementIdByName} from './bpmn-elements.js'; +import type {BpmnElement, BpmnSemantic, BpmnVisualization} from 'bpmn-visualization'; import {getActivityRecommendationData} from './recommendation-data.js'; +import {type CaseMonitoringData, getCaseMonitoringData} from './case-monitoring-data.js'; +import {currentView, displayBpmnDiagram, secondaryBpmnVisualization} from './diagram.js'; const tippyInstances: Instance[] = []; const registeredBpmnElements = new Map(); -let alreadyExecutedElements = new Set(); +let caseMonitoringData: CaseMonitoringData; -export function showCaseMonitoringData(bpmnVisualization: BpmnVisualization) { - const alreadyExecutedShapes = getAlreadyExecutedShapes(); - const alreadyVisitedEdges = getConnectingEdgeIds(new Set([...alreadyExecutedShapes, ...(getRunningActivities()), ...getPendingElements()]), bpmnVisualization); - alreadyExecutedElements = new Set([...alreadyExecutedShapes, ...alreadyVisitedEdges]); +export function showCaseMonitoringData(processId: string, bpmnVisualization: BpmnVisualization) { + caseMonitoringData = getCaseMonitoringData(processId, bpmnVisualization); reduceVisibilityOfAlreadyExecutedElements(bpmnVisualization); highlightRunningElements(bpmnVisualization); + highlightEnabledElements(bpmnVisualization); // eslint-disable-next-line no-warning-comments -- cannot be managed now // TODO what is it for? // registerInteractions(bpmnVisualization); } -export function hideCaseMonitoringData(bpmnVisualization: BpmnVisualization) { +export function hideCaseMonitoringData(processId: string, bpmnVisualization: BpmnVisualization) { + caseMonitoringData = getCaseMonitoringData(processId, bpmnVisualization); restoreVisibilityOfAlreadyExecutedElements(bpmnVisualization); resetRunningElements(bpmnVisualization); } -// Already executed shapes: activities, gateways, events, ... -function getAlreadyExecutedShapes() { - const alreadyExecutedShapes = new Set(); - addNonNullElement(alreadyExecutedShapes, getElementIdByName('New POI Needed')); // Start event - addNonNullElement(alreadyExecutedShapes, 'Gateway_0xh0plz'); // Parallel gateway after start event - addNonNullElement(alreadyExecutedShapes, getElementIdByName('Vendor Creates Invoice')); - addNonNullElement(alreadyExecutedShapes, getElementIdByName('Create Purchase Order Item')); - return alreadyExecutedShapes; -} - -function addNonNullElement(elements: Set, elt: string | undefined) { - if (elt) { - elements.add(elt); - } -} - -function getRunningActivities() { - const runningActivities = new Set(); - addNonNullElement(runningActivities, getElementIdByName('SRM subprocess')); - return runningActivities; -} - -function getPendingElements() { - return new Set(['Gateway_0domayw']); -} - function reduceVisibilityOfAlreadyExecutedElements(bpmnVisualization: BpmnVisualization) { - bpmnVisualization.bpmnElementsRegistry.addCssClasses(Array.from(alreadyExecutedElements), 'state-already-executed'); + bpmnVisualization.bpmnElementsRegistry.addCssClasses([...caseMonitoringData.executedShapes, ...caseMonitoringData.visitedEdges], 'state-already-executed'); } function restoreVisibilityOfAlreadyExecutedElements(bpmnVisualization: BpmnVisualization) { - bpmnVisualization.bpmnElementsRegistry.removeCssClasses(Array.from(alreadyExecutedElements), 'state-already-executed'); + // eslint-disable-next-line no-warning-comments -- question to answer by Nour + // TODO why adding pending? the CSS class was not added in reduceVisibilityOfAlreadyExecutedElements + bpmnVisualization.bpmnElementsRegistry.removeCssClasses([...caseMonitoringData.executedShapes, ...caseMonitoringData.pendingShapes, ...caseMonitoringData.visitedEdges], 'state-already-executed'); } // eslint-disable-next-line no-warning-comments -- cannot be managed now // TODO: rename CSS class function highlightRunningElements(bpmnVisualization: BpmnVisualization) { - const runningActivities = getRunningActivities(); - bpmnVisualization.bpmnElementsRegistry.addCssClasses(Array.from(runningActivities), 'state-predicted-late'); - for (const activityId of runningActivities) { - addPopover(activityId, bpmnVisualization); - addOverlay(activityId, bpmnVisualization); + const elements = caseMonitoringData.runningActivities; + bpmnVisualization.bpmnElementsRegistry.addCssClasses(elements, 'state-running-late'); + if (currentView === 'main') { + addInfoOnRunningElements(elements, bpmnVisualization); + } +} + +export function highlightEnabledElements(bpmnVisualization: BpmnVisualization) { + const elements = caseMonitoringData.enabledShapes; + bpmnVisualization.bpmnElementsRegistry.addCssClasses(elements, 'state-enabled'); + if (currentView === 'secondary') { + addInfoOnEnabledElements(elements, bpmnVisualization); } } function resetRunningElements(bpmnVisualization: BpmnVisualization) { - const runningActivities = Array.from(getRunningActivities()); - bpmnVisualization.bpmnElementsRegistry.removeCssClasses(runningActivities, 'state-predicted-late'); - for (const activityId of runningActivities) { + const elements = caseMonitoringData.runningActivities; + bpmnVisualization.bpmnElementsRegistry.removeCssClasses(elements, 'state-running-late'); + for (const activityId of elements) { bpmnVisualization.bpmnElementsRegistry.removeAllOverlays(activityId); } @@ -88,38 +72,25 @@ function resetRunningElements(bpmnVisualization: BpmnVisualization) { tippyInstances.length = 0; } -function getConnectingEdgeIds(shapeIds: Set, bpmnVisualization: BpmnVisualization) { - const edgeIds = new Set(); - for (const shape of shapeIds) { - const shapeElt = bpmnVisualization.bpmnElementsRegistry.getElementsByIds(shape)[0]; - const bpmnSemantic = shapeElt.bpmnSemantic as ShapeBpmnSemantic; - const incomingEdges = bpmnSemantic.incomingIds; - const outgoingEdges = bpmnSemantic.outgoingIds; - for (const edgeId of incomingEdges) { - const edgeElement = bpmnVisualization.bpmnElementsRegistry.getElementsByIds(edgeId)[0]; - const sourceRef = (edgeElement.bpmnSemantic as EdgeBpmnSemantic).sourceRefId; - if (shapeIds.has(sourceRef)) { - edgeIds.add(edgeId); - } - } - - for (const edgeId of outgoingEdges) { - const edgeElement = bpmnVisualization.bpmnElementsRegistry.getElementsByIds(edgeId)[0]; - const targetRef = (edgeElement.bpmnSemantic as EdgeBpmnSemantic).targetRefId; - if (shapeIds.has(targetRef)) { - edgeIds.add(edgeId); - } - } +function addInfoOnRunningElements(bpmnElementIds: string[], bpmnVisualization: BpmnVisualization) { + for (const bpmnElementId of bpmnElementIds) { + addPopover(bpmnElementId, bpmnVisualization); + addOverlay(bpmnElementId, bpmnVisualization); } +} - return edgeIds; +function addInfoOnEnabledElements(bpmnElementIds: string[], bpmnVisualization: BpmnVisualization) { + for (const bpmnElementId of bpmnElementIds) { + addPopover(bpmnElementId, bpmnVisualization); + addOverlay(bpmnElementId, bpmnVisualization); + } } -function addPopover(activityId: string, bpmnVisualization: BpmnVisualization) { - const activity = bpmnVisualization.bpmnElementsRegistry.getElementsByIds(activityId)[0]; - registerBpmnElement(activity); +function addPopover(bpmnElementId: string, bpmnVisualization: BpmnVisualization) { + const bpmnElement = bpmnVisualization.bpmnElementsRegistry.getElementsByIds(bpmnElementId)[0]; + registerBpmnElement(bpmnElement); - const tippyInstance = tippy(activity.htmlElement, { + const tippyInstance = tippy(bpmnElement.htmlElement, { theme: 'light', placement: 'bottom', appendTo: bpmnVisualization.graph.container, @@ -130,36 +101,46 @@ function addPopover(activityId: string, bpmnVisualization: BpmnVisualization) { allowHTML: true, trigger: 'mouseenter', onShown(instance: Instance): void { - instance.setContent(getRecommendationInfoAsHtml(instance.reference)); + if (currentView === 'main') { + instance.setContent(getRecommendationInfoAsHtml(instance.reference)); + // eslint-disable-next-line no-warning-comments -- cannot be managed now + // TODO avoid hard coding or manage this in the same class that generate 'getRecommendationInfoAsHtml' + // eslint-disable-next-line no-warning-comments -- cannot be managed now + // TODO only register the event listener once, or destroy it onHide + const contactClientBtn = document.querySelector('#Contact-Client'); + console.info('tippy on show: contactClientBtn', contactClientBtn); + if (contactClientBtn) { + console.info('tippy on show: registering event listener on click'); + contactClientBtn.addEventListener('click', () => { + showContactClientAction(); + }); + } + + const allocateResourceBtn = document.querySelector('#Allocate-Resource'); + console.info('tippy on show: allocateResourceBtn', allocateResourceBtn); + if (allocateResourceBtn) { + console.info('tippy on show: registering event listener on click'); + allocateResourceBtn.addEventListener('click', () => { + showResourceAllocationAction(); + }); + } + } else { + instance.setContent(getWarningInfoAsHtml()); + } }, } as Partial); tippyInstances.push(tippyInstance); - - // eslint-disable-next-line no-warning-comments -- cannot be managed now - // TODO make it work - // get references to the buttons in the Tippy popover - // const allocateResourceBtn = tippyInstance.popper.querySelector('#Allocate-Resource'); - // const contactClientBtn = tippyInstance.popper.querySelector('#Contact-Client'); - - // add event listeners to the buttons - /* allocateResourceBtn.addEventListener('click', function() { - //showhMonitoringDataSubProcess(); - }); - - contactClientBtn.addEventListener('click', function() { - //complete - }); */ } -function addOverlay(activityId: string, bpmnVisualization: BpmnVisualization) { - bpmnVisualization.bpmnElementsRegistry.addOverlays(activityId, { - position: 'top-center', - label: ' ℹ️ ', +function addOverlay(bpmnElementId: string, bpmnVisualization: BpmnVisualization) { + bpmnVisualization.bpmnElementsRegistry.addOverlays(bpmnElementId, { + position: 'top-right', + label: '?', style: { font: {color: '#fff', size: 16}, - fill: {color: 'rgba(0, 0, 0, 0.5)'}, - stroke: {color: 'rgba(0, 0, 0, 0.5)', width: 2}, + fill: {color: '#4169E1'}, + stroke: {color: '#4169E1', width: 2}, }, }); } @@ -171,19 +152,15 @@ function registerBpmnElement(bpmnElement: BpmnElement) { function getRecommendationInfoAsHtml(htmlElement: ReferenceElement) { let popoverContent = `
- - - - - - - - `; +

Task running late

+

Here are some suggestions:

+
Recommendation
+ `; const bpmnSemantic = registeredBpmnElements.get(htmlElement); const activityRecommendationData = getActivityRecommendationData(bpmnSemantic?.name ?? ''); for (const recommendation of activityRecommendationData) { - // Replace space with hypen (-) to be passed as the button id + // Replace space with hyphen (-) to be passed as the button id const buttonId = recommendation.title.replace(/\s+/g, '-'); popoverContent += ` @@ -203,3 +180,88 @@ function getRecommendationInfoAsHtml(htmlElement: ReferenceElement) { `; return popoverContent; } + +function getWarningInfoAsHtml() { +// Function getWarningInfoAsHtml(htmlElement: ReferenceElement) { + return ` +
+

Resource not available

+

The resource "pierre" is not available to execute this task.

+

Here are some other suggestions:

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Resource NameAvailability
Resource 1Yes + +
Resource 2Yes + +
Resource 3Yes + +
+
+ `; +} + +function showResourceAllocationAction() { + displayBpmnDiagram('secondary'); + showCaseMonitoringData('secondary', secondaryBpmnVisualization); + /* + TO FIX: currently the code assumes that there's only one enabled shape + */ + // TO COMPLETE: add interaction on the popover: on hover, highlight some activities + const enabledShapeId = caseMonitoringData.enabledShapes.values().next().value as string; + const enabledShape = secondaryBpmnVisualization.bpmnElementsRegistry.getElementsByIds(enabledShapeId)[0]; + const popoverInstance = tippyInstances.find(instance => { + if (instance.reference === enabledShape?.htmlElement) { + return instance; + } + + return null; + }); + + if (popoverInstance) { + // Add additional actions to the existing mouseover event listener + /* + The listener is NOT WORKING + */ + popoverInstance.popper.addEventListener('mouseover', (event: MouseEvent) => { + const target = event.target as HTMLElement; + console.info('listener mouseover, target', target); + // If (target.nodeName === 'TD') { + // const selectedRow = target.parentElement as HTMLTableRowElement; + // } + }); + } else { + console.log('instance not found'); + } +} + +function showContactClientAction() { + // eslint-disable-next-line no-warning-comments -- cannot be managed now + // TODO implement + // eslint-disable-next-line no-alert -- will be remove with the final implementation + window.alert('Clicked on showContactClientAction'); +} + diff --git a/src/diagram.ts b/src/diagram.ts index ad0f751..890bab9 100644 --- a/src/diagram.ts +++ b/src/diagram.ts @@ -6,12 +6,15 @@ import {removeSectionInBreadcrumb, addSectionInBreadcrumb} from './breadcrumb.js const sharedFitOptions = {type: FitType.Center, margin: 20}; export const sharedLoadOptions: LoadOptions = {fit: sharedFitOptions}; - -let secondaryBpmnDiagramIsAlreadyLoad = false; -let currentView = 'main'; +// eslint-disable-next-line no-warning-comments -- cannot be managed now +// TODO do not leak internal state +// eslint-disable-next-line import/no-mutable-exports -- will be refactored later +export let secondaryBpmnDiagramIsAlreadyLoad = false; +// eslint-disable-next-line import/no-mutable-exports -- will be refactored later +export let currentView = 'main'; // Secondary BPMN Container -const secondaryBpmnVisualization = new BpmnVisualization({container: 'secondary-bpmn-container'}); +export const secondaryBpmnVisualization = new BpmnVisualization({container: 'secondary-bpmn-container'}); export function displayBpmnDiagram(tabIndex: string): void { if (currentView === tabIndex) { diff --git a/src/style.css b/src/style.css index 6371c8d..0663a64 100644 --- a/src/style.css +++ b/src/style.css @@ -134,12 +134,8 @@ header { :root { --color-default-fill: white; --color-path-executed: #aea3a3; - --color-path-predicted-late: orange; - --color-state-predicted-late: orange; - --color-path-predicted-on-time: #67d567; - --color-state-predicted-on-time: #a7f0a7; - --color-state-running: #9c9cef; - --stroke-path-predicted: 0.18rem; + --color-state-running-late: orange; + --color-state-enabled: #d3d3d3; } /* parallel gateway icon */ @@ -176,35 +172,17 @@ header { filter: blur(1px); } -/* ================================================================================================================== */ -/* INFO */ -/* ================================================================================================================== */ - -/* only the surrounding path of gateway, otherwise the diamond is darker (double fill) */ -.bpmn-type-gateway.state-running > path:nth-child(1), -.bpmn-type-event.state-running > ellipse, -/* envelope of the message event */ -.bpmn-event-def-message.state-running > rect { - fill: var(--color-state-running); -} - -/*apply shadow on hover*/ -.bpmn-type-event.state-running:hover, -.bpmn-type-gateway.state-running:hover { - filter: drop-shadow(0 0 0.5em rgba(0, 0, 0)); -} - /* ================================================================================================================== */ -/* PREDICTED LATE */ +/* RUNNING LATE */ /* ================================================================================================================== */ /*for filter drop-shadow: https://css-tricks.com/adding-shadows-to-svg-icons-with-css-and-svg-filters/*/ /*for pulse animation: https://reactgo.com/css-pulse-animation/*/ /* task */ -.bpmn-type-activity.state-predicted-late > rect { - fill: var(--color-state-predicted-late); +.bpmn-type-activity.state-running-late > rect { + fill: var(--color-state-running-late); animation: pulse-animation 0.8s infinite alternate; } @@ -219,45 +197,42 @@ header { } } - /* message flow start marker */ -.bpmn-message-flow.path-predicted-late > ellipse, - /* sequence flow arrow */ -.bpmn-sequence-flow.path-predicted-late > path:nth-child(3) { - fill: var(--color-path-predicted-late); -} +/* ================================================================================================================== */ +/* ENABLED */ +/* ================================================================================================================== */ - /* sequence/message flow line and arrow (end marker) */ -.bpmn-type-flow.path-predicted-late > path, - /* message flow start marker */ -.bpmn-message-flow.path-predicted-late > ellipse, -.bpmn-type-gateway.path-predicted-late > path:nth-child(1), -.bpmn-type-task.path-predicted-late > rect, -.bpmn-type-event.path-predicted-late > ellipse { - stroke: var(--color-path-predicted-late); - stroke-width: var(--stroke-path-predicted); +/* task */ +.bpmn-type-activity.state-enabled> rect { + fill: var(--color-state-enabled); } -.bpmn-type-gateway.path-predicted-late > path:nth-child(1), -.bpmn-event-based-gateway.path-predicted-late > ellipse, -.bpmn-event-based-gateway.path-predicted-late > path:nth-child(7), -.bpmn-event-def-message.path-predicted-late > rect, -.bpmn-event-def-message.path-predicted-late > path, -.bpmn-event-def-timer.path-predicted-late > path, -.bpmn-type-event.path-predicted-late > ellipse { - stroke: var(--color-path-predicted-late); - fill: var(--color-default-fill); +/* parallel gateway icon */ +.bpmn-parallel-gateway.state-enabled > path:nth-child(2), +/* message flow arrow */ +.bpmn-message-flow.state-enabled > path:nth-child(4){ + fill: var(--color-state-enabled); } -/* labels (the selector applies to all div, not the only one that contains text, but this is ok. -Use important to override the color set inline in the style attribute of the label div */ -.bpmn-label.path-predicted-late > g div { - color: var(--color-path-predicted-late) !important; - font-weight: bold; + /* message flow start marker */ +.bpmn-message-flow.state-enabled > ellipse, + /* sequence/message flow line and arrow (end marker) */ +.bpmn-type-flow.state-enabled > path, + /* task */ +.bpmn-type-task.state-enabled > rect, + /* parallel gateway stroke and icon */ +.bpmn-parallel-gateway.state-enabled > path, + /* message event icon */ +.bpmn-event-def-message.state-enabled > rect, +.bpmn-event-def-message.state-enabled > path, + /* event stroke */ +.bpmn-type-event.state-enabled > ellipse { + fill: var(--color-state-enabled); } /* ================================================================================================================== */ /* RECOMMENDATION POPOVER */ +/* More generally case-monitoring */ /* ================================================================================================================== */ /*padding: 10px; @@ -275,6 +250,11 @@ th.popover-title { padding: 10px; } +tr.popover-row:hover { + background-color: lightgray; + color: black; +} + td.popover-key { font-weight: bold; padding: 5px; @@ -307,7 +287,3 @@ button { button:hover { background-color: #3e8e41; } - -/*.popover-row:nth-child(even) {*/ -/* background-color: #f2f2f2;*/ -/*}*/ diff --git a/src/use-case-management.ts b/src/use-case-management.ts index 04b5ffb..179f89c 100644 --- a/src/use-case-management.ts +++ b/src/use-case-management.ts @@ -1,7 +1,7 @@ import {type BpmnVisualization} from 'bpmn-visualization'; import {hideCaseMonitoringData, showCaseMonitoringData} from './case-monitoring.js'; import {hideHappyPath, showHappyPath} from './process-monitoring.js'; -import {ProcessVisualizer, SubProcessNavigator} from './diagram.js'; +import {ProcessVisualizer, secondaryBpmnDiagramIsAlreadyLoad, secondaryBpmnVisualization, SubProcessNavigator} from './diagram.js'; export function configureUseCaseSelectors(bpmnVisualization: BpmnVisualization) { const processVisualizer = new ProcessVisualizer(bpmnVisualization); @@ -19,9 +19,12 @@ export function configureUseCaseSelectors(bpmnVisualization: BpmnVisualization) // eslint-disable-next-line no-new new UseCaseSelector('radio-case-monitoring', () => { processVisualizer.hideManuallyTriggeredProcess(); - showCaseMonitoringData(bpmnVisualization); + showCaseMonitoringData('main', bpmnVisualization); }, () => { - hideCaseMonitoringData(bpmnVisualization); + hideCaseMonitoringData('main', bpmnVisualization); + if (secondaryBpmnDiagramIsAlreadyLoad) { + hideCaseMonitoringData('secondary', secondaryBpmnVisualization); + } }); const initialUseCase = new UseCaseSelector('radio-reset-all', () => { diff --git a/src/utils/bpmn-elements.ts b/src/utils/bpmn-elements.ts new file mode 100644 index 0000000..a097b47 --- /dev/null +++ b/src/utils/bpmn-elements.ts @@ -0,0 +1,76 @@ +/* +Copyright 2023 Bonitasoft S.A. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {type BpmnSemantic, type BpmnVisualization, ShapeBpmnElementKind, ShapeUtil} from 'bpmn-visualization'; + +/** + * Provides workarounds for {@link https://github.com/process-analytics/bpmn-visualization-js/issues/2453}. + */ +export class BpmnElementsSearcher { + constructor(private readonly bpmnVisualization: BpmnVisualization) {} + + getElementIdByName(name: string): string | undefined { + return this.getElementByName(name)?.id; + } + + // Only work for shape for now + // not optimize, do a full lookup at each call + private getElementByName(name: string): BpmnSemantic | undefined { + const kinds = Object.values(ShapeBpmnElementKind); + // Split query by kind to avoid returning a big chunk of data + for (const kind of kinds) { + const bpmnSemantics = this.bpmnVisualization.bpmnElementsRegistry.getElementsByKinds(kind) + .map(elt => elt.bpmnSemantic) + .filter(Boolean); + + // May have been implemented with: bpmnSemantics.filter(bpmnSemantic => bpmnSemantic.name === name)[0]; + // Here, we stop the search right after we find a matching name + for (const bpmnSemantic of bpmnSemantics) { + if (bpmnSemantic.name === name) { + return bpmnSemantic; + } + } + } + + return undefined; + } +} + +export class BpmnElementsIdentifier { + constructor(private readonly bpmnVisualization: BpmnVisualization) {} + + isActivity(elementId: string): boolean { + return this.isInCategory(ShapeUtil.isActivity, elementId); + } + + isGateway(elementId: string): boolean { + return this.isInCategory(ShapeUtil.isGateway, elementId); + } + + isEvent(elementId: string): boolean { + return this.isInCategory(ShapeUtil.isEvent, elementId); + } + + private isInCategory(categorizeFunction: (value: string) => boolean, elementId: string): boolean { + const elements = this.bpmnVisualization.bpmnElementsRegistry.getElementsByIds(elementId); + if (elements.length > 0) { + const kind = elements[0].bpmnSemantic.kind; + return categorizeFunction(kind); + } + + return false; + } +} diff --git a/src/utils/paths.ts b/src/utils/paths.ts new file mode 100644 index 0000000..79c1648 --- /dev/null +++ b/src/utils/paths.ts @@ -0,0 +1,53 @@ +/* +Copyright 2023 Bonitasoft S.A. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type {BpmnVisualization, EdgeBpmnSemantic, ShapeBpmnSemantic} from 'bpmn-visualization'; + +/** + * Experimental implementation for + * - {@link https://github.com/process-analytics/bpmn-visualization-js/issues/930} + * - {@link https://github.com/process-analytics/bpmn-visualization-js/issues/2402} + */ +export class PathResolver { + constructor(private readonly bpmnVisualization: BpmnVisualization) {} + + getVisitedEdges(shapeIds: string[]): string[] { + const edgeIds = new Set(); + for (const shape of shapeIds) { + const shapeElt = this.bpmnVisualization.bpmnElementsRegistry.getElementsByIds(shape)[0]; + const bpmnSemantic = shapeElt.bpmnSemantic as ShapeBpmnSemantic; + const incomingEdges = bpmnSemantic.incomingIds; + const outgoingEdges = bpmnSemantic.outgoingIds; + for (const edgeId of incomingEdges) { + const edgeElement = this.bpmnVisualization.bpmnElementsRegistry.getElementsByIds(edgeId)[0]; + const sourceRef = (edgeElement.bpmnSemantic as EdgeBpmnSemantic).sourceRefId; + if (shapeIds.includes(sourceRef)) { + edgeIds.add(edgeId); + } + } + + for (const edgeId of outgoingEdges) { + const edgeElement = this.bpmnVisualization.bpmnElementsRegistry.getElementsByIds(edgeId)[0]; + const targetRef = (edgeElement.bpmnSemantic as EdgeBpmnSemantic).targetRefId; + if (shapeIds.includes(targetRef)) { + edgeIds.add(edgeId); + } + } + } + + return Array.from(edgeIds); + } +}