diff --git a/src/provider/bpmn/BpmnPropertiesProvider.js b/src/provider/bpmn/BpmnPropertiesProvider.js index 79d59e769..18c40aa38 100644 --- a/src/provider/bpmn/BpmnPropertiesProvider.js +++ b/src/provider/bpmn/BpmnPropertiesProvider.js @@ -237,8 +237,9 @@ function getGroups(element, injector) { export default class BpmnPropertiesProvider { constructor(propertiesPanel, injector) { - propertiesPanel.registerProvider(this); this._injector = injector; + + propertiesPanel.registerProvider(this); } getGroups(element) { diff --git a/src/provider/camunda-platform/CamundaPlatformPropertiesProvider.js b/src/provider/camunda-platform/CamundaPlatformPropertiesProvider.js index b51dd34e9..710fba590 100644 --- a/src/provider/camunda-platform/CamundaPlatformPropertiesProvider.js +++ b/src/provider/camunda-platform/CamundaPlatformPropertiesProvider.js @@ -106,9 +106,9 @@ const CAMUNDA_PLATFORM_GROUPS = [ export default class CamundaPlatformPropertiesProvider { constructor(propertiesPanel, injector) { - propertiesPanel.registerProvider(LOW_PRIORITY, this); - this._injector = injector; + + propertiesPanel.registerProvider(LOW_PRIORITY, this); } getGroups(element) { diff --git a/src/provider/zeebe/ZeebePropertiesProvider.js b/src/provider/zeebe/ZeebePropertiesProvider.js index 388934aab..b840dbea8 100644 --- a/src/provider/zeebe/ZeebePropertiesProvider.js +++ b/src/provider/zeebe/ZeebePropertiesProvider.js @@ -76,9 +76,9 @@ const ZEEBE_GROUPS = [ export default class ZeebePropertiesProvider { constructor(propertiesPanel, injector) { - propertiesPanel.registerProvider(LOW_PRIORITY, this); - this._injector = injector; + + propertiesPanel.registerProvider(LOW_PRIORITY, this); } getGroups(element) { diff --git a/src/render/BpmnPropertiesPanel.js b/src/render/BpmnPropertiesPanel.js index 4c83e22a8..6d058be3a 100644 --- a/src/render/BpmnPropertiesPanel.js +++ b/src/render/BpmnPropertiesPanel.js @@ -1,16 +1,3 @@ -import { - useState, - useMemo, - useEffect, - useCallback -} from '@bpmn-io/properties-panel/preact/hooks'; - -import { - find, - isArray, - reduce -} from 'min-dash'; - import { FeelLanguageContext, PropertiesPanel } from '@bpmn-io/properties-panel'; import { @@ -28,7 +15,6 @@ const DEFAULT_FEEL_LANGUAGE_CONTEXT = { * @param {Object} props * @param {djs.model.Base|Array} [props.element] * @param {Injector} props.injector - * @param { (djs.model.Base) => Array } props.getProviders * @param {Object} props.layoutConfig * @param {Object} props.descriptionConfig * @param {Object} props.tooltipConfig @@ -38,194 +24,38 @@ const DEFAULT_FEEL_LANGUAGE_CONTEXT = { export default function BpmnPropertiesPanel(props) { const { element, + groups, injector, - getProviders, - layoutConfig: initialLayoutConfig, + layoutConfig, descriptionConfig, tooltipConfig, feelPopupContainer, getFeelPopupLinks } = props; - const canvas = injector.get('canvas'); - const elementRegistry = injector.get('elementRegistry'); const eventBus = injector.get('eventBus'); const translate = injector.get('translate'); - const [ state, setState ] = useState({ - selectedElement: element - }); - - const selectedElement = state.selectedElement; - - /** - * @param {djs.model.Base | Array} element - */ - const _update = (element) => { - - if (!element) { - return; - } - - let newSelectedElement = element; - - // handle labels - if (newSelectedElement && newSelectedElement.type === 'label') { - newSelectedElement = newSelectedElement.labelTarget; - } - - setState({ - ...state, - selectedElement: newSelectedElement - }); - - // notify interested parties on property panel updates - eventBus.fire('propertiesPanel.updated', { - element: newSelectedElement - }); - }; - - // (2) react on element changes - - // (2a) selection changed - useEffect(() => { - const onSelectionChanged = (e) => { - const { newSelection = [] } = e; - - if (newSelection.length > 1) { - return _update(newSelection); - } - - const newElement = newSelection[0]; - - const rootElement = canvas.getRootElement(); - - if (isImplicitRoot(rootElement)) { - return; - } - - _update(newElement || rootElement); - }; - - eventBus.on('selection.changed', onSelectionChanged); - - return () => { - eventBus.off('selection.changed', onSelectionChanged); - }; - }, []); - - // (2b) selected element changed - useEffect(() => { - const onElementsChanged = (e) => { - const elements = e.elements; - - const updatedElement = findElement(elements, selectedElement); - - if (updatedElement && elementExists(updatedElement, elementRegistry)) { - _update(updatedElement); - } - }; - - eventBus.on('elements.changed', onElementsChanged); - - return () => { - eventBus.off('elements.changed', onElementsChanged); - }; - }, [ selectedElement ]); + const selectedElement = element; - // (2c) root element changed - useEffect(() => { - const onRootAdded = (e) => { - const element = e.element; - - _update(element); - }; - - eventBus.on('root.added', onRootAdded); - - return () => { - eventBus.off('root.added', onRootAdded); - }; - }, [ selectedElement ]); - - // (2d) provided entries changed - useEffect(() => { - const onProvidersChanged = () => { - _update(selectedElement); - }; - - eventBus.on('propertiesPanel.providersChanged', onProvidersChanged); - - return () => { - eventBus.off('propertiesPanel.providersChanged', onProvidersChanged); - }; - }, [ selectedElement ]); - - // (2e) element templates changed - useEffect(() => { - const onTemplatesChanged = () => { - _update(selectedElement); - }; - - eventBus.on('elementTemplates.changed', onTemplatesChanged); - - return () => { - eventBus.off('elementTemplates.changed', onTemplatesChanged); - }; - }, [ selectedElement ]); - - // (3) create properties panel context const bpmnPropertiesPanelContext = { selectedElement, injector, getService(type, strict) { return injector.get(type, strict); } }; - // (4) retrieve groups for selected element - const providers = getProviders(selectedElement); - - const groups = useMemo(() => { - return reduce(providers, function(groups, provider) { - - // do not collect groups for multi element state - if (isArray(selectedElement)) { - return []; - } - - const updater = provider.getGroups(selectedElement); - - return updater(groups); - }, []); - }, [ providers, selectedElement ]); - - // (5) notify layout changes - const [ layoutConfig, setLayoutConfig ] = useState(initialLayoutConfig || {}); - - const onLayoutChanged = useCallback((newLayout) => { + const onLayoutChanged = (layoutConfig) => { eventBus.fire('propertiesPanel.layoutChanged', { - layout: newLayout + layout: layoutConfig }); - }, [ eventBus ]); - - // React to external layout changes - useEffect(() => { - const cb = (e) => { - const { layout } = e; - setLayoutConfig(layout); - }; - - eventBus.on('propertiesPanel.setLayout', cb); - return () => eventBus.off('propertiesPanel.setLayout', cb); - }, [ eventBus, setLayoutConfig ]); + }; - // (6) notify description changes const onDescriptionLoaded = (description) => { eventBus.fire('propertiesPanel.descriptionLoaded', { description }); }; - // (7) notify tooltip changes const onTooltipLoaded = (tooltip) => { eventBus.fire('propertiesPanel.tooltipLoaded', { tooltip @@ -253,18 +83,3 @@ export default function BpmnPropertiesPanel(props) { ); } - - -// helpers ////////////////////////// - -function isImplicitRoot(element) { - return element && element.isImplicit; -} - -function findElement(elements, element) { - return find(elements, (e) => e === element); -} - -function elementExists(element, elementRegistry) { - return element && elementRegistry.get(element.id); -} diff --git a/src/render/BpmnPropertiesPanelRenderer.js b/src/render/BpmnPropertiesPanelRenderer.js index 489ae6eb2..e555de58d 100644 --- a/src/render/BpmnPropertiesPanelRenderer.js +++ b/src/render/BpmnPropertiesPanelRenderer.js @@ -15,6 +15,8 @@ import { event as domEvent } from 'min-dom'; +import { isArray, reduce } from 'min-dash'; + const DEFAULT_PRIORITY = 1000; /** @@ -47,7 +49,7 @@ export default class BpmnPropertiesPanelRenderer { '
' ); - var commandStack = injector.get('commandStack', false); + const commandStack = injector.get('commandStack', false); commandStack && setupKeyboard(this._container, eventBus, commandStack); @@ -61,13 +63,16 @@ export default class BpmnPropertiesPanelRenderer { this.detach(); }); - eventBus.on('root.added', (event) => { - const { element } = event; + this._selectedElement = null; + this._groups = []; - this._render(element); - }); - } + eventBus.on('selection.changed', () => this._update()); + eventBus.on('elements.changed', () => this._update()); + eventBus.on('elementTemplates.changed', () => this._update()); + eventBus.on('propertiesPanel.providersChanged', () => this._update()); + eventBus.on('propertiesPanel.setLayout', event => this._updateLayout(event)); + } /** * Attach the properties panel to a parent node. @@ -158,22 +163,16 @@ export default class BpmnPropertiesPanelRenderer { return event.providers; } - _render(element) { - const canvas = this._injector.get('canvas'); - - if (!element) { - element = canvas.getRootElement(); - } - - if (isImplicitRoot(element)) { + _render() { + if (!this._selectedElement || isImplicitRoot(this._selectedElement)) { return; } render( 1) { + this._selectedElement = selectedElements; + } else if (selectedElements.length === 1) { + let newSelectedElement = selectedElements[0]; + + // handle labels + if (newSelectedElement.type === 'label') { + newSelectedElement = newSelectedElement.labelTarget; + } + + this._selectedElement = newSelectedElement; + } else { + this._selectedElement = rootElement; + } + } + + _updateGroups() { + if (!this._selectedElement || isImplicitRoot(this._selectedElement) || isArray(this._selectedElement)) { + this._groups = []; + + return; + } + + const providers = this._getProviders(this._selectedElement); + + this._groups = reduce(providers, (groups, provider) => { + const updater = provider.getGroups(this._selectedElement); + + return updater(groups); + }, []); + } + + _updateLayout({ layout }) { + this._layoutConfig = layout; + + this._render(); + + this._eventBus.fire('propertiesPanel.updated', { + element: this._selectedElement + }); + } } BpmnPropertiesPanelRenderer.$inject = [ 'config.propertiesPanel', 'injector', 'eventBus' ]; diff --git a/test/spec/BpmnPropertiesPanel.spec.js b/test/spec/BpmnPropertiesPanel.spec.js index 310591ed8..d955aa279 100644 --- a/test/spec/BpmnPropertiesPanel.spec.js +++ b/test/spec/BpmnPropertiesPanel.spec.js @@ -1,7 +1,4 @@ -import { - act, - render -} from '@testing-library/preact/pure'; +import { render } from '@testing-library/preact/pure'; import TestContainer from 'mocha-test-container-support'; @@ -13,8 +10,7 @@ import { import { Injector as injectorMock, ElementRegistry as elementRegistryMock, - EventBus as eventBusMock, - getProviders as getProvidersMock + EventBus as eventBusMock } from './mocks'; import { @@ -69,7 +65,7 @@ describe('', function() { it('should render provided groups', function() { // given - const groups1 = [ + const groups = [ { id: 'group-1', label: 'Group 1', @@ -87,30 +83,11 @@ describe('', function() { } ]; - const groups2 = [ - { - id: 'group-4', - label: 'Group 4', - entries: [] - } - ]; - - const getProviders = () => { - return [ - { - getGroups: () => (groups) => groups.concat(groups1) - }, - { - getGroups: () => (groups) => groups.concat(groups2) - } - ]; - }; - // when - const result = createBpmnPropertiesPanel({ container, getProviders }); + const result = createBpmnPropertiesPanel({ container, groups }); // then - expect(domQueryAll('.bio-properties-panel-group', result.container)).to.have.length(4); + expect(domQueryAll('.bio-properties-panel-group', result.container)).to.have.length(3); }); @@ -141,12 +118,8 @@ describe('', function() { const eventBus = new eventBusMock(); - const result = createBpmnPropertiesPanel({ container, eventBus }); - // when - await act(() => { - eventBus.fire('selection.changed', { newSelection: newElements }); - }); + const result = createBpmnPropertiesPanel({ container, eventBus, element: newElements }); // then expect(domQuery('.bio-properties-panel-placeholder', result.container)).to.exist; @@ -155,189 +128,26 @@ describe('', function() { }); - describe('event emitting', function() { + describe('events', function() { - it('should update on selection changed', function() { + it('should emit on layout changed', function() { // given const updateSpy = sinon.spy(); const eventBus = new eventBusMock(); - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus }); + eventBus.on('propertiesPanel.layoutChanged', updateSpy); // when - eventBus.fire('selection.changed', { newSelection: [ noopElement ] }); - - // then - expect(updateSpy).to.have.been.calledWith({ - element: noopElement - }); - }); - - - it('should update on selection changed - multiple', async function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - const elements = [ - noopElement, - noopElement - ]; - - eventBus.on('propertiesPanel.updated', updateSpy); - createBpmnPropertiesPanel({ container, eventBus }); - // when - eventBus.fire('selection.changed', { newSelection: elements }); - - // then - expect(updateSpy).to.have.been.calledWith({ - element: elements - }); - }); - - - it('should update on element changed', function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus }); - - // when - eventBus.fire('elements.changed', { elements: [ noopElement ] }); - - // then - expect(updateSpy).to.have.been.calledWith({ - element: noopElement - }); - }); - - - it('should update on root element changed', function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus }); - - // when - eventBus.fire('root.added', { element: noopElement }); - - // then - expect(updateSpy).to.have.been.calledWith({ - element: noopElement - }); - }); - - - it('should update on providers changed', function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus }); - - // when - eventBus.fire('propertiesPanel.providersChanged'); - - // then - expect(updateSpy).to.have.been.calledOnce; - }); - - - it('should update on element templates changed', function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus }); - - // when - eventBus.fire('elementTemplates.changed'); - // then expect(updateSpy).to.have.been.calledOnce; }); - describe('layout', function() { - - it('should notify on layout changed', function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - eventBus.on('propertiesPanel.layoutChanged', updateSpy); - - // when - createBpmnPropertiesPanel({ container, eventBus }); - - // then - expect(updateSpy).to.have.been.calledOnce; - }); - - - withPropertiesPanel('>=1.5.0')('should react to external changes', async function() { - - // given - const originalLayout = { - open: false - }; - const newLayout = { - open: true - }; - - const updateSpy = sinon.spy(); - const eventBus = new eventBusMock(); - eventBus.on('propertiesPanel.layoutChanged', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus, layoutConfig: originalLayout }); - - // assume - expect(updateSpy).to.have.been.calledWith(sinon.match({ layout: originalLayout })); - - updateSpy.resetHistory(); - - // when - await act(() => { - eventBus.fire('propertiesPanel.setLayout', { layout: newLayout }); - }); - - // then - expect(updateSpy).to.have.been.calledOnce; - expect(updateSpy.lastCall).to.have.been.calledWith(sinon.match({ layout: newLayout })); - }); - - }); - - - it('should notify on description loaded', function() { + it('should emit on description loaded', function() { // given const loadedSpy = sinon.spy(); @@ -354,7 +164,7 @@ describe('', function() { }); - it('should notify on tooltip loaded', function() { + it('should emit on tooltip loaded', function() { // given const loadedSpy = sinon.spy(); @@ -370,65 +180,6 @@ describe('', function() { expect(loadedSpy).to.have.been.called; }); - - it('should notify on properties panel changed', function() { - - // given - const updateSpy = sinon.spy(); - - const eventBus = new eventBusMock(); - - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ container, eventBus }); - - // when - eventBus.fire('propertiesPanel.providersChanged'); - - // then - expect(updateSpy).to.have.been.calledOnce; - }); - - - it('should not update deleted element', async function() { - - // given - const element = { - ...noopElement, - id: 'B', - type: 'foo:Deleted' - }; - - let elements = [ - element, - noopElement, - noopElement - ]; - - const elementRegistry = new elementRegistryMock(); - elementRegistry.setElements(elements); - - const updateSpy = sinon.spy(); - const eventBus = new eventBusMock(); - eventBus.on('propertiesPanel.updated', updateSpy); - - createBpmnPropertiesPanel({ - container, - element, - elementRegistry, - eventBus - }); - - // when --> remove the currently selected element - elements.splice(0, 1); - elementRegistry.setElements(elements); - - eventBus.fire('elements.changed', { elements: [ element ] }); - - // then - expect(updateSpy).to.not.have.been.called; - }); - }); }); @@ -440,7 +191,7 @@ function createBpmnPropertiesPanel(options = {}) { const { element = noopElement, - getProviders = getProvidersMock, + groups = [], layoutConfig, descriptionConfig, descriptionLoaded, @@ -466,8 +217,8 @@ function createBpmnPropertiesPanel(options = {}) { return render( ', function() { }); - it('should render on root.added', async function() { + describe('rendering', function() { - // given - const diagramXml = require('test/fixtures/simple.bpmn').default; + it('should render', async function() { - // when - await createModeler(diagramXml); + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + // when + await createModeler(diagramXml); + + // then + expect(domQuery('.bio-properties-panel', propertiesContainer)).to.exist; + }); + + + it('should rerender on selection changed (single element)', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + const element = modeler.get('elementRegistry').get('Task_1'); + + await act(() => { + modeler.get('selection').select(element); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('.bio-properties-panel-header-type', propertiesContainer).textContent).to.equal('Task'); + }); + + + it('should rerender on selection changed (multi element)', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + const elements = [ + modeler.get('elementRegistry').get('Task_1'), + modeler.get('elementRegistry').get('EndEvent_1') + ]; + + await act(() => { + modeler.get('selection').select(elements); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('.bio-properties-panel-placeholder-text', propertiesContainer).textContent).to.equal('Multiple elements are selected. Select a single element to edit its properties.'); + }); + + + it('should rerender on selection changed (no element)', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + await act(() => { + modeler.get('selection').select(null); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('.bio-properties-panel-header-type', propertiesContainer).textContent).to.equal('Process'); + }); + + + it('should rerender on selection changed (label)', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + const label = modeler.get('elementRegistry').get('StartEvent_1').label; + + // when + await act(() => { + modeler.get('selection').select(label); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('.bio-properties-panel-header-type', propertiesContainer).textContent).to.equal('Start Event'); + }); + + + it('should rerender on providers changed', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + await act(() => { + modeler.get('propertiesPanel').registerProvider({ + getGroups: () => { + return (groups) => { + return [ + ...groups, + { + id: 'foo-group', + label: 'Foo Group', + entries: [] + } + ]; + }; + } + }); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('.bio-properties-panel-group-header-title[data-title="Foo Group"]', propertiesContainer)).to.exist; + }); + + + it('should rerender on element templates changed', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + await act(() => { + modeler.get('eventBus').fire('elementTemplates.changed'); + }); + + // then + expect(spy).to.have.been.called; + }); + + + it('should rerender on elements changed', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + await act(() => { + const element = modeler.get('elementRegistry').get('Process_1'); + + modeler.get('modeling').updateProperties(element, { name: 'Foo Process' }); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('#bio-properties-panel-name', propertiesContainer).value).to.equal('Foo Process'); + }); + + + withPropertiesPanel('>=1.5.0')('should rerender on layout changed', async function() { + + // given + const diagramXml = require('test/fixtures/simple.bpmn').default; + + const { modeler } = await createModeler(diagramXml); + + const spy = sinon.spy(); + + modeler.on('propertiesPanel.updated', spy); + + // when + await act(() => { + modeler.get('eventBus').fire('propertiesPanel.setLayout', { + layout: { + groups: { + general: { + open: true + } + } + } + }); + }); + + // then + expect(spy).to.have.been.called; + expect(domQuery('.bio-properties-panel-group[data-group-id="group-general"] .bio-properties-panel-group-header.open', propertiesContainer)).to.exist; + }); - // then - expect(domQuery('.bio-properties-panel', propertiesContainer)).to.exist; }); @@ -355,7 +565,7 @@ describe('', function() { // when const propertiesPanel = modeler.get('propertiesPanel'); propertiesPanel.attachTo(propertiesContainer); - propertiesPanel._render(rootElement); + propertiesPanel._render(); // then expect(domQuery('.bio-properties-panel', propertiesContainer)).to.not.exist; @@ -508,6 +718,7 @@ describe('', function() { expect(getHeaderName(propertiesContainer)).to.eql('start'); }); + describe('input', function() { let modeler;