feat(search): be able to search bpmn elements

Closes #500
This commit is contained in:
Vladimirs Katusenoks 2016-03-27 16:04:07 +02:00 committed by pedesen
parent 60720c8ae7
commit bff19786b4
7 changed files with 379 additions and 4 deletions

View File

@ -169,6 +169,7 @@ Modeler.prototype._interactionModules = [
// non-modeling components // non-modeling components
require('./features/label-editing'), require('./features/label-editing'),
require('./features/auto-resize'), require('./features/auto-resize'),
require('./features/search'),
require('diagram-js/lib/navigation/zoomscroll'), require('diagram-js/lib/navigation/zoomscroll'),
require('diagram-js/lib/navigation/movecanvas'), require('diagram-js/lib/navigation/movecanvas'),
require('diagram-js/lib/navigation/touch') require('diagram-js/lib/navigation/touch')

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing, function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing,
selection, canvas, elementRegistry, editorActions) { searchPad, selection, canvas, elementRegistry, editorActions) {
var actions = { var actions = {
selectElements: function() { selectElements: function() {
@ -30,6 +30,9 @@ function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing
if (currentSelection.length) { if (currentSelection.length) {
directEditing.activate(currentSelection[0]); directEditing.activate(currentSelection[0]);
} }
},
find: function() {
searchPad.toggle();
} }
}; };
@ -44,6 +47,13 @@ function BpmnKeyBindings(keyboard, spaceTool, lassoTool, handTool, directEditing
return true; return true;
} }
// ctrl + f -> search labels
if (key === 70 && keyboard.isCmd(modifiers)) {
editorActions.trigger('find');
return true;
}
if (keyboard.hasModifier(modifiers)) { if (keyboard.hasModifier(modifiers)) {
return; return;
} }
@ -84,6 +94,7 @@ BpmnKeyBindings.$inject = [
'lassoTool', 'lassoTool',
'handTool', 'handTool',
'directEditing', 'directEditing',
'searchPad',
'selection', 'selection',
'canvas', 'canvas',
'elementRegistry', 'elementRegistry',

View File

@ -0,0 +1,120 @@
'use strict';
var map = require('lodash/collection/map'),
filter = require('lodash/collection/filter'),
sortBy = require('lodash/collection/sortBy');
var labelUtil = require('../label-editing/LabelUtil');
/**
* Provides ability to search through BPMN elements
*/
function Search(elementRegistry, searchPad) {
this._elementRegistry = elementRegistry;
searchPad.registerProvider(this);
}
module.exports = Search;
Search._inject = [
'elementRegistry',
'searchPad'
];
/**
* Finds all elements that match given pattern
*
* <Result> :
* {
* primaryTokens: <Array<Token>>,
* secondaryTokens: <Array<Token>>,
* element: <Element>
* }
*
* <Token> :
* {
* normal|matched: <String>
* }
*
* @param {String} pattern
* @return {Array<Result>}
*/
Search.prototype.find = function(pattern) {
var elements = this._elementRegistry.filter(function(element) {
if (element.labelTarget) {
return false;
}
return true;
});
elements = map(elements, function(element) {
return {
primaryTokens: matchAndSplit(labelUtil.getLabel(element), pattern),
secondaryTokens: matchAndSplit(element.id, pattern),
element: element
};
});
// exclude non-matched elements
elements = filter(elements, function(element) {
return hasMatched(element.primaryTokens) || hasMatched(element.secondaryTokens);
});
elements = sortBy(elements, function(element) {
return labelUtil.getLabel(element.element) + element.element.id;
});
return elements;
};
function hasMatched(tokens) {
var matched = filter(tokens, function(t){
return !!t.matched;
});
return matched.length > 0;
}
function matchAndSplit(text, pattern) {
var tokens = [],
originalText = text;
if (!text) {
return tokens;
}
text = text.toLowerCase();
pattern = pattern.toLowerCase();
var i = text.indexOf(pattern);
if (i > -1) {
if (i !== 0) {
tokens.push({
normal: originalText.substr(0, i)
});
}
tokens.push({
matched: originalText.substr(i, pattern.length)
});
if (pattern.length + i < text.length) {
tokens.push({
normal: originalText.substr(pattern.length + i, text.length)
});
}
} else {
tokens.push({
normal: originalText
});
}
return tokens;
}

View File

@ -0,0 +1,7 @@
module.exports = {
__depends__: [
require('diagram-js/lib/features/search-pad')
],
__init__: [ 'bpmnSearch'],
bpmnSearch: [ 'type', require('./BpmnSearchProvider') ]
};

View File

@ -1,12 +1,11 @@
'use strict'; 'use strict';
var TestHelper = require('../../../TestHelper');
var TestContainer = require('mocha-test-container-support'); var TestContainer = require('mocha-test-container-support');
var coreModule = require('../../../../lib/core'), var coreModule = require('../../../../lib/core'),
modelingModule = require('../../../../lib/features/modeling'), modelingModule = require('../../../../lib/features/modeling'),
keyboardModule = require('../../../../lib/features/keyboard'), keyboardModule = require('../../../../lib/features/keyboard'),
bpmnSearchModule = require('../../../../lib/features/search'),
selectionModule = require('diagram-js/lib/features/selection'), selectionModule = require('diagram-js/lib/features/selection'),
spaceToolModule = require('diagram-js/lib/features/space-tool'), spaceToolModule = require('diagram-js/lib/features/space-tool'),
lassoToolModule = require('diagram-js/lib/features/lasso-tool'), lassoToolModule = require('diagram-js/lib/features/lasso-tool'),
@ -27,6 +26,7 @@ describe('features - keyboard', function() {
modelingModule, modelingModule,
selectionModule, selectionModule,
spaceToolModule, spaceToolModule,
bpmnSearchModule,
lassoToolModule, lassoToolModule,
handToolModule, handToolModule,
keyboardModule, keyboardModule,
@ -46,7 +46,7 @@ describe('features - keyboard', function() {
it('should include triggers inside editorActions', inject(function(editorActions) { it('should include triggers inside editorActions', inject(function(editorActions) {
// then // then
expect(editorActions.length()).to.equal(11); expect(editorActions.length()).to.equal(12);
})); }));
@ -117,6 +117,21 @@ describe('features - keyboard', function() {
expect(selectedElements).not.to.contain(rootElement); expect(selectedElements).not.to.contain(rootElement);
})); }));
it('should trigger search for labels', inject(function(canvas, keyboard, searchPad, elementRegistry) {
sinon.spy(searchPad, 'toggle');
// given
var e = createKeyEvent(container, 70, true);
// when
keyboard._keyHandler(e);
// then
expect(searchPad.toggle).calledOnce;
}));
}); });
}); });

View File

@ -0,0 +1,121 @@
'use strict';
var coreModule = require('../../../../lib/core'),
modelingModule = require('../../../../lib/features/modeling'),
bpmnSearchModule = require('../../../../lib/features/search');
/* global bootstrapViewer, inject */
describe('features - BPMN search provider', function() {
var diagramXML = require('./bpmn-search.bpmn');
var testModules = [
coreModule,
modelingModule,
bpmnSearchModule
];
beforeEach(bootstrapViewer(diagramXML, { modules: testModules }));
it('find should return all elements that match label or ID', inject(function(bpmnSearch) {
// given
var pattern = '123456';
// when
var elements = bpmnSearch.find(pattern);
// then
expect(elements).length(3);
elements.forEach(function(e) {
expect(e).to.have.property('element');
expect(e).to.have.property('primaryTokens');
expect(e).to.have.property('secondaryTokens');
});
}));
it('matches IDs', inject(function(bpmnSearch) {
// given
var pattern = 'datastore';
// when
var elements = bpmnSearch.find(pattern);
// then
expect(elements[0].primaryTokens).to.eql([
{ normal: 'has matched ID'}
]);
expect(elements[0].secondaryTokens).to.eql([
{ normal: 'some_'},
{ matched: 'DataStore'},
{ normal: '_123456_id'},
]);
}));
describe('should split result into matched and non matched tokens', function() {
it('matched all', inject(function(bpmnSearch) {
// given
var pattern = 'all matched';
// when
var elements = bpmnSearch.find(pattern);
// then
expect(elements[0].primaryTokens).to.eql([
{ matched: 'all matched'}
]);
}));
it('matched start', inject(function(bpmnSearch) {
// given
var pattern = 'before';
// when
var elements = bpmnSearch.find(pattern);
// then
expect(elements[0].primaryTokens).to.eql([
{ matched: 'before'},
{ normal: ' 321'}
]);
}));
it('matched middle', inject(function(bpmnSearch) {
// given
var pattern = 'middle';
// when
var elements = bpmnSearch.find(pattern);
// then
expect(elements[0].primaryTokens).to.eql([
{ normal: '123 '},
{ matched: 'middle'},
{ normal: ' 321'}
]);
}));
it('matched end', inject(function(bpmnSearch) {
// given
var pattern = 'after';
// when
var elements = bpmnSearch.find(pattern);
// then
expect(elements[0].primaryTokens).to.eql([
{ normal: '123 '},
{ matched: 'after'}
]);
}));
});
});

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:task id="Task_1j5i0e6" name="Second 123456 task here">
<bpmn:outgoing>SequenceFlow_0wgiusn</bpmn:outgoing>
<bpmn:dataOutputAssociation id="DataOutputAssociation_1jomsz7">
<bpmn:targetRef>some_DataStore_123456_id</bpmn:targetRef>
</bpmn:dataOutputAssociation>
</bpmn:task>
<bpmn:intermediateThrowEvent id="IntermediateThrowEvent_1lhurmj" name="Third 123456">
<bpmn:incoming>SequenceFlow_0wgiusn</bpmn:incoming>
</bpmn:intermediateThrowEvent>
<bpmn:sequenceFlow id="SequenceFlow_0wgiusn" sourceRef="Task_1j5i0e6" targetRef="IntermediateThrowEvent_1lhurmj" />
<bpmn:dataStoreReference id="some_DataStore_123456_id" name="has matched ID" />
<bpmn:task id="Task_0dso4ju" name="UNIQUE ELEMENT" />
<bpmn:task id="Task_asdfasd" name="before 321">
<bpmn:incoming>SequenceFlow_1bhe9h2</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_02ymelh</bpmn:outgoing>
</bpmn:task>
<bpmn:task id="Task_asdfasddgg" name="123 middle 321">
<bpmn:incoming>SequenceFlow_02ymelh</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_0ugwp0d</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_02ymelh" sourceRef="Task_asdfasd" targetRef="Task_asdfasddgg" />
<bpmn:task id="Task_asdfasdsdfgg" name="123 after">
<bpmn:incoming>SequenceFlow_0ugwp0d</bpmn:incoming>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_0ugwp0d" sourceRef="Task_asdfasddgg" targetRef="Task_asdfasdsdfgg" />
<bpmn:task id="Task_0vuhy0s" name="all matched">
<bpmn:outgoing>SequenceFlow_1bhe9h2</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="SequenceFlow_1bhe9h2" sourceRef="Task_0vuhy0s" targetRef="Task_asdfasd" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="Task_1j5i0e6_di" bpmnElement="Task_1j5i0e6">
<dc:Bounds x="195" y="106" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="IntermediateThrowEvent_1lhurmj_di" bpmnElement="IntermediateThrowEvent_1lhurmj">
<dc:Bounds x="227" y="299" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="200" y="335" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0wgiusn_di" bpmnElement="SequenceFlow_0wgiusn">
<di:waypoint xsi:type="dc:Point" x="245" y="186" />
<di:waypoint xsi:type="dc:Point" x="245" y="299" />
<bpmndi:BPMNLabel>
<dc:Bounds x="339" y="265" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="DataStoreReference_093mpev_di" bpmnElement="some_DataStore_123456_id">
<dc:Bounds x="371" y="121" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="351" y="186" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0dso4ju_di" bpmnElement="Task_0dso4ju">
<dc:Bounds x="0" y="0" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_0vyzyuo_di" bpmnElement="Task_asdfasd">
<dc:Bounds x="622.5883069427528" y="246.96102314250913" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1wkhcs9_di" bpmnElement="Task_asdfasddgg">
<dc:Bounds x="783" y="247" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_02ymelh_di" bpmnElement="SequenceFlow_02ymelh">
<di:waypoint xsi:type="dc:Point" x="723" y="287" />
<di:waypoint xsi:type="dc:Point" x="783" y="287" />
<bpmndi:BPMNLabel>
<dc:Bounds x="708" y="277" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_1m7fa4o_di" bpmnElement="Task_asdfasdsdfgg">
<dc:Bounds x="939" y="247" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_0ugwp0d_di" bpmnElement="SequenceFlow_0ugwp0d">
<di:waypoint xsi:type="dc:Point" x="883" y="287" />
<di:waypoint xsi:type="dc:Point" x="939" y="287" />
<bpmndi:BPMNLabel>
<dc:Bounds x="866" y="277" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Task_0vuhy0s_di" bpmnElement="Task_0vuhy0s">
<dc:Bounds x="471" y="247" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="SequenceFlow_1bhe9h2_di" bpmnElement="SequenceFlow_1bhe9h2">
<di:waypoint xsi:type="dc:Point" x="571" y="287" />
<di:waypoint xsi:type="dc:Point" x="623" y="287" />
<bpmndi:BPMNLabel>
<dc:Bounds x="552" y="277" width="90" height="20" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="DataOutputAssociation_1jomsz7_di" bpmnElement="DataOutputAssociation_1jomsz7">
<di:waypoint xsi:type="dc:Point" x="295" y="146" />
<di:waypoint xsi:type="dc:Point" x="371" y="146" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>