Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(BpmnElementsSearcher): provide options to deduplicate elements #131

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions packages/addons/src/bpmn-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,53 @@ import { FlowKind, ShapeBpmnElementKind, ShapeUtil as BaseShapeUtil } from 'bpmn

const allBpmnElementKinds: BpmnElementKind[] = [...Object.values(ShapeBpmnElementKind), ...Object.values(FlowKind)];

/**
* Options to deduplicate elements when several names match.
*/
export type DeduplicateNamesOptions = {
/** If not set, use all `BpmnElementKind` values. */
kinds?: BpmnElementKind[];
/** Apply custom function to filter duplicates. */
filter?: (bpmnSemantic: BpmnSemantic) => boolean;
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const acceptAll = (_bpmnSemantic: BpmnSemantic): boolean => true;

/**
* Provides workarounds for {@link https://github.com/process-analytics/bpmn-visualization-js/issues/2453}.
*/
export class BpmnElementsSearcher {
constructor(private readonly elementsRegistry: ElementsRegistry) {}

/**
* Find the ID of the first element that matches the provided name.
* @param name the name of the element to retrieve.
*/
getElementIdByName(name: string): string | undefined {
return this.getElementByName(name)?.id;
}

// not optimize, do a full lookup at each call
private getElementByName(name: string): BpmnSemantic | undefined {
/**
* Find the element that matches the provided name.
*
* Use the `deduplicateOptions` parameter to modify the default behavior of deduplication processing.
*
* The deduplication process is done in this order:
* - look for elements matching the provided kinds. If not specified, use all `BpmnElementKind` values.
* - apply the deduplication filter if provided, otherwise take the first element corresponding to the name provided.
*
* @param name the name of the element to retrieve.
* @param deduplicateOptions if not defined, or if the object doesn't define any properties, duplicates are filtered out by selecting the first element corresponding to the name provided.
*/
getElementByName(name: string, deduplicateOptions?: DeduplicateNamesOptions): BpmnSemantic | undefined {
// Not optimized, do a full lookup at each call
// Split query by kind to avoid returning a big chunk of data
for (const kind of allBpmnElementKinds) {
const candidate = this.elementsRegistry.getModelElementsByKinds(kind).find(element => element.name === name);
for (const kind of deduplicateOptions?.kinds ?? allBpmnElementKinds) {
const candidate = this.elementsRegistry
.getModelElementsByKinds(kind)
.filter(element => element.name === name)
.find(element => (deduplicateOptions?.filter ?? acceptAll)(element));
if (candidate) {
return candidate;
}
Expand Down
39 changes: 20 additions & 19 deletions packages/addons/test/fixtures/bpmn/search-elements.bpmn
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1" targetNamespace="http://example.com/schema/bpmn">
<bpmn:collaboration id="Collaboration_1jke0md">
<bpmn:participant id="Participant_0onpt9o" processRef="Process_1" />
<bpmn:participant id="Participant_0zym2fg" processRef="Process_0qq1mgm" />
<bpmn:participant id="Participant_1" processRef="Process_1" />
<bpmn:participant id="Participant_2" processRef="Process_2" />
<bpmn:messageFlow id="messageFlow_1" name="message flow 1" sourceRef="Event_1fo3lwp" targetRef="Event_0wvq8ch" />
</bpmn:collaboration>
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1" name="start event 1">
<bpmn:outgoing>Flow_1yf7yd6</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Task_1" name="task 1">
<bpmn:task id="Task_1" name="task 1 with duplicate name">
<bpmn:incoming>Flow_1yf7yd6</bpmn:incoming>
<bpmn:outgoing>Flow_0th6cj1</bpmn:outgoing>
</bpmn:task>
Expand All @@ -27,10 +27,10 @@
<bpmn:incoming>Flow_1xozzqt</bpmn:incoming>
<bpmn:outgoing>Flow_1cj2f9n</bpmn:outgoing>
</bpmn:task>
<bpmn:task id="Task_with_same_name_as_Task_1" name="task 1">
<bpmn:userTask id="UserTask_with_same_name_as_Task_1" name="task 1 with duplicate name">
<bpmn:incoming>Flow_1a9vtky</bpmn:incoming>
<bpmn:outgoing>Flow_0i4ule4</bpmn:outgoing>
</bpmn:task>
</bpmn:userTask>
<bpmn:exclusiveGateway id="Gateway_0t7d2lu" name="gateway 2">
<bpmn:incoming>Flow_0qnma25</bpmn:incoming>
<bpmn:incoming>Flow_045a06d</bpmn:incoming>
Expand All @@ -46,10 +46,10 @@
<bpmn:sequenceFlow id="Flow_0th6cj1" sourceRef="Task_1" targetRef="Gateway_1" />
<bpmn:sequenceFlow id="Flow_18zkq4t" sourceRef="Gateway_1" targetRef="Task_2_1" />
<bpmn:sequenceFlow id="Flow_1xozzqt" sourceRef="Gateway_1" targetRef="Activity_08z13ne" />
<bpmn:sequenceFlow id="Flow_1a9vtky" sourceRef="Gateway_1" targetRef="Task_with_same_name_as_Task_1" />
<bpmn:sequenceFlow id="Flow_1a9vtky" sourceRef="Gateway_1" targetRef="UserTask_with_same_name_as_Task_1" />
<bpmn:sequenceFlow id="Flow_045a06d" sourceRef="Task_2_1" targetRef="Gateway_0t7d2lu" />
<bpmn:sequenceFlow id="Flow_1cj2f9n" sourceRef="Activity_08z13ne" targetRef="Gateway_0t7d2lu" />
<bpmn:sequenceFlow id="Flow_0i4ule4" sourceRef="Task_with_same_name_as_Task_1" targetRef="Event_0wvq8ch" />
<bpmn:sequenceFlow id="Flow_0i4ule4" sourceRef="UserTask_with_same_name_as_Task_1" targetRef="Event_0wvq8ch" />
<bpmn:sequenceFlow id="Flow_0qnma25" sourceRef="Event_0wvq8ch" targetRef="Gateway_0t7d2lu" />
<bpmn:endEvent id="Event_1hr2hqx" name="end event 1">
<bpmn:incoming>Flow_0p544v1</bpmn:incoming>
Expand All @@ -58,16 +58,16 @@
<bpmn:textAnnotation id="TextAnnotation_1">
<bpmn:text>Duplicated name on purpose</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_0fwvz81" sourceRef="Task_with_same_name_as_Task_1" targetRef="TextAnnotation_1" />
<bpmn:association id="Association_0fwvz81" sourceRef="UserTask_with_same_name_as_Task_1" targetRef="TextAnnotation_1" />
</bpmn:process>
<bpmn:process id="Process_0qq1mgm">
<bpmn:sequenceFlow id="sequenceFlow_10" name="seq flow 10" sourceRef="Event_1ndg0m5" targetRef="Activity_1rwjkd0" />
<bpmn:sequenceFlow id="sequenceFlow_11" name="seq flow 11" sourceRef="Activity_1rwjkd0" targetRef="Event_1fo3lwp" />
<bpmn:process id="Process_2">
<bpmn:sequenceFlow id="sequenceFlow_10" name="seq flow 10" sourceRef="Event_1ndg0m5" targetRef="Task_with_duplicated_name_with_textAnnotations" />
<bpmn:sequenceFlow id="sequenceFlow_11" name="seq flow 11" sourceRef="Task_with_duplicated_name_with_textAnnotations" targetRef="Event_1fo3lwp" />
<bpmn:sequenceFlow id="sequenceFlow_with_same_name_as_sequenceFlow_11" name="seq flow 11" sourceRef="Event_1fo3lwp" targetRef="Event_0md1mpw" />
<bpmn:startEvent id="Event_1ndg0m5">
<bpmn:outgoing>sequenceFlow_10</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="Activity_1rwjkd0">
<bpmn:task id="Task_with_duplicated_name_with_textAnnotations" name="Duplicated name on purpose">
<bpmn:incoming>sequenceFlow_10</bpmn:incoming>
<bpmn:outgoing>sequenceFlow_11</bpmn:outgoing>
</bpmn:task>
Expand All @@ -79,14 +79,14 @@
<bpmn:outgoing>sequenceFlow_with_same_name_as_sequenceFlow_11</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_03dnjxq" />
</bpmn:intermediateThrowEvent>
<bpmn:textAnnotation id="TextAnnotation_13ptcyf">
<bpmn:textAnnotation id="TextAnnotation_2">
<bpmn:text>Duplicated name on purpose</bpmn:text>
</bpmn:textAnnotation>
<bpmn:association id="Association_1ovp59n" sourceRef="sequenceFlow_with_same_name_as_sequenceFlow_11" targetRef="TextAnnotation_13ptcyf" />
<bpmn:association id="Association_1ovp59n" sourceRef="sequenceFlow_with_same_name_as_sequenceFlow_11" targetRef="TextAnnotation_2" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_1jke0md">
<bpmndi:BPMNShape id="Participant_0onpt9o_di" bpmnElement="Participant_0onpt9o" isHorizontal="true">
<bpmndi:BPMNShape id="Participant_1_di" bpmnElement="Participant_1" isHorizontal="true">
<dc:Bounds x="119" y="58" width="939" height="450" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
Expand All @@ -113,7 +113,7 @@
<dc:Bounds x="520" y="190" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_with_same_name_as_Task_1_di" bpmnElement="Task_with_same_name_as_Task_1">
<bpmndi:BPMNShape id="UserTask_with_same_name_as_Task_1_di" bpmnElement="UserTask_with_same_name_as_Task_1">
<dc:Bounds x="520" y="310" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
Expand Down Expand Up @@ -184,14 +184,15 @@
<di:waypoint x="532" y="390" />
<di:waypoint x="494" y="430" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Participant_0zym2fg_di" bpmnElement="Participant_0zym2fg" isHorizontal="true">
<bpmndi:BPMNShape id="Participant_2_di" bpmnElement="Participant_2" isHorizontal="true">
<dc:Bounds x="119" y="540" width="939" height="250" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1ndg0m5_di" bpmnElement="Event_1ndg0m5">
<dc:Bounds x="172" y="672" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1rwjkd0_di" bpmnElement="Activity_1rwjkd0">
<bpmndi:BPMNShape id="Task_with_duplicated_name_with_textAnnotations_di" bpmnElement="Task_with_duplicated_name_with_textAnnotations">
<dc:Bounds x="290" y="650" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0md1mpw_di" bpmnElement="Event_0md1mpw">
<dc:Bounds x="872" y="672" width="36" height="36" />
Expand All @@ -202,7 +203,7 @@
<bpmndi:BPMNShape id="Event_0qpts3g_di" bpmnElement="Event_1fo3lwp">
<dc:Bounds x="682" y="672" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="TextAnnotation_13ptcyf_di" bpmnElement="TextAnnotation_13ptcyf">
<bpmndi:BPMNShape id="TextAnnotation_2_di" bpmnElement="TextAnnotation_2">
<dc:Bounds x="780" y="730" width="100" height="55" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
Expand Down
133 changes: 103 additions & 30 deletions packages/addons/test/spec/bpmn-elements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,116 @@ import { BpmnElementsIdentifier, BpmnElementsSearcher, BpmnVisualization } from
import { createNewBpmnVisualizationWithoutContainer } from '../shared/bv-utils';
import { insertBpmnContainerWithoutId } from '../shared/dom-utils';
import { readFileSync } from '../shared/io-utils';
import type { ShapeBpmnSemantic } from 'bpmn-visualization';
import { ShapeBpmnElementKind } from 'bpmn-visualization';

describe('Find element ids by providing names', () => {
describe('Find elements by providing names', () => {
const bpmnVisualization = new BpmnVisualization({ container: insertBpmnContainerWithoutId() });
bpmnVisualization.load(readFileSync('./fixtures/bpmn/search-elements.bpmn'));
const bpmnElementsSearcher = new BpmnElementsSearcher(bpmnVisualization.bpmnElementsRegistry);

const getModelElementName = (bpmnElementId: string): string => bpmnVisualization.bpmnElementsRegistry.getModelElementsByIds(bpmnElementId).map(element => element.name)[0];

test.each([
{ name: 'start event 1', expectedId: 'StartEvent_1' },
{ name: 'gateway 1', expectedId: 'Gateway_1' },
{ name: 'seq flow 10', expectedId: 'sequenceFlow_10' },
{ name: 'message flow 1', expectedId: 'messageFlow_1' },
])('an existing element - $name', ({ name, expectedId }: { name: string; expectedId: string }) => {
expect(bpmnElementsSearcher.getElementIdByName(name)).toBe(expectedId);
});

test('several existing tasks with the same name', () => {
// Verify that several elements have the same names
expect(getModelElementName('Task_1')).toBe('task 1');
expect(getModelElementName('Task_with_same_name_as_Task_1')).toBe('task 1');

// Retrieve the first one
expect(bpmnElementsSearcher.getElementIdByName('task 1')).toBe('Task_1');
const expectElementsHavingTheSameName = (bpmnElementIds: string[], expectedName: string): void => {
for (const bpmnElementId of bpmnElementIds) {
const elementName = bpmnVisualization.bpmnElementsRegistry.getModelElementsByIds(bpmnElementId).map(element => element.name)[0];
expect(elementName).toBe(expectedName);
}
};

describe('Retrieve ids only', () => {
test.each([
{ name: 'start event 1', expectedId: 'StartEvent_1' },
{ name: 'gateway 1', expectedId: 'Gateway_1' },
{ name: 'seq flow 10', expectedId: 'sequenceFlow_10' },
{ name: 'message flow 1', expectedId: 'messageFlow_1' },
])('an existing element - $name', ({ name, expectedId }: { name: string; expectedId: string }) => {
expect(bpmnElementsSearcher.getElementIdByName(name)).toBe(expectedId);
});

test('several existing tasks with the same name', () => {
expectElementsHavingTheSameName(['Task_1', 'UserTask_with_same_name_as_Task_1'], 'task 1 with duplicate name');

// Retrieve the first one
expect(bpmnElementsSearcher.getElementIdByName('task 1 with duplicate name')).toBe('Task_1');
});

test('several existing sequence flows with the same name', () => {
expectElementsHavingTheSameName(['sequenceFlow_11', 'sequenceFlow_with_same_name_as_sequenceFlow_11'], 'seq flow 11');

// Retrieve the first one
expect(bpmnElementsSearcher.getElementIdByName('seq flow 11')).toBe('sequenceFlow_11');
});

test('unknown element', () => {
expect(bpmnElementsSearcher.getElementIdByName('nobody knows me')).toBeUndefined();
});
});

test('several existing sequence flows with the same name', () => {
// Verify that several elements have the same names
expect(getModelElementName('sequenceFlow_11')).toBe('seq flow 11');
expect(getModelElementName('sequenceFlow_with_same_name_as_sequenceFlow_11')).toBe('seq flow 11');

// Retrieve the first one
expect(bpmnElementsSearcher.getElementIdByName('seq flow 11')).toBe('sequenceFlow_11');
});

test('unknown element', () => {
expect(bpmnElementsSearcher.getElementIdByName('nobody knows me')).toBeUndefined();
describe('Retrieve objects', () => {
test('Several existing tasks with the same name with default options', () => {
expectElementsHavingTheSameName(['Task_1', 'UserTask_with_same_name_as_Task_1'], 'task 1 with duplicate name');

// Retrieve the first one
expect(bpmnElementsSearcher.getElementByName('task 1 with duplicate name')).toEqual({
id: 'Task_1',
incomingIds: ['Flow_1yf7yd6'],
isShape: true,
kind: ShapeBpmnElementKind.TASK,
name: 'task 1 with duplicate name',
outgoingIds: ['Flow_0th6cj1'],
parentId: 'Participant_1',
} as ShapeBpmnSemantic);
});

test('Several existing tasks with the same name - deduplicate with kinds', () => {
expectElementsHavingTheSameName(['Task_1', 'UserTask_with_same_name_as_Task_1'], 'task 1 with duplicate name');

expect(bpmnElementsSearcher.getElementByName('task 1 with duplicate name', { kinds: [ShapeBpmnElementKind.TASK_USER] })).toEqual({
id: 'UserTask_with_same_name_as_Task_1',
incomingIds: ['Flow_1a9vtky'],
isShape: true,
kind: ShapeBpmnElementKind.TASK_USER,
name: 'task 1 with duplicate name',
outgoingIds: ['Flow_0i4ule4', 'Association_0fwvz81'],
parentId: 'Participant_1',
} as ShapeBpmnSemantic);
});

test('Several existing tasks with the same name - deduplicate with the filtering function', () => {
expectElementsHavingTheSameName(['Task_1', 'UserTask_with_same_name_as_Task_1'], 'task 1 with duplicate name');

expect(
bpmnElementsSearcher.getElementByName('task 1 with duplicate name', {
filter: bpmnSemantic => bpmnSemantic.isShape && (bpmnSemantic as ShapeBpmnSemantic).outgoingIds.includes('Association_0fwvz81'),
}),
).toEqual({
id: 'UserTask_with_same_name_as_Task_1',
incomingIds: ['Flow_1a9vtky'],
isShape: true,
kind: ShapeBpmnElementKind.TASK_USER,
name: 'task 1 with duplicate name',
outgoingIds: ['Flow_0i4ule4', 'Association_0fwvz81'],
parentId: 'Participant_1',
} as ShapeBpmnSemantic);
});

test('Several existing tasks with the same name - deduplicate with both kinds and the filtering function', () => {
expectElementsHavingTheSameName(['TextAnnotation_1', 'TextAnnotation_2', 'Task_with_duplicated_name_with_textAnnotations'], 'Duplicated name on purpose');

expect(
bpmnElementsSearcher.getElementByName('Duplicated name on purpose', {
kinds: [ShapeBpmnElementKind.TEXT_ANNOTATION],
filter: bpmnSemantic => bpmnSemantic.isShape && (bpmnSemantic as ShapeBpmnSemantic).parentId == 'Participant_2',
}),
).toEqual({
id: 'TextAnnotation_2',
incomingIds: ['Association_1ovp59n'],
isShape: true,
kind: ShapeBpmnElementKind.TEXT_ANNOTATION,
name: 'Duplicated name on purpose',
outgoingIds: [],
parentId: 'Participant_2',
} as ShapeBpmnSemantic);
});
});
});

Expand Down