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); + } +}