diff --git a/.github/workflows/run-npm-linting.yml b/.github/workflows/run-npm-linting.yml new file mode 100644 index 00000000..3e572e28 --- /dev/null +++ b/.github/workflows/run-npm-linting.yml @@ -0,0 +1,28 @@ +name: Check the code style of the Quantum Workflow Modeler + +on: + pull_request: + types: [ opened, synchronize ] + push: + branches: + - 'master' + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install Node.js dependencies and run linters + run: | + npm ci + npm run lint + working-directory: ./components/bpmn-q diff --git a/.github/workflows/run-npm-test.yml b/.github/workflows/run-npm-test.yml index e0135496..af1661d7 100644 --- a/.github/workflows/run-npm-test.yml +++ b/.github/workflows/run-npm-test.yml @@ -1,4 +1,4 @@ -name: Run all test of the Quantum Workflow Modeler +name: Run Tests on: pull_request: diff --git a/NOTICE b/NOTICE index d4a1bb4e..850d5745 100644 --- a/NOTICE +++ b/NOTICE @@ -1,16 +1,12 @@ - The source code of this project is licensed under the Apache 2.0 license, found in the LICENSE file in this directory. -The code of the QuantME Plugin und certain utility functions of the editor are based on and using code from the -QuantME Transformation Framework, licensed under the MIT license and available -under https://github.com/UST-QuAntiL/QuantME-TransformationFramework. +The code of the QuantME Plugin and certain utility functions of the editor are based on and using code from the QuantME Transformation Framework, licensed under the MIT license and available under https://github.com/UST-QuAntiL/QuantME-TransformationFramework. The code of the PlanQK Plugin is based on the code of the workflow modeler prototype of the PlanQK Platform. -Certain components of the editor use and extend the code of the bpmn-js repository, the diagram-js repository and the camunda-modeler -repository of Camunda. The bpmn-js repository is licensed under the bpmn.io license and available in the following GitHub -repository https://github.com/bpmn-io/bpmn-js. The diagram-js repository is licensed under the MIT license, available under -the following link: https://github.com/bpmn-io/diagram-js. The camunda-modeler is licensed under the MIT license and available -under the following link: https://github.com/camunda/camunda-modeler. +Certain components of the editor use and extend the code of the bpmn-js repository, the diagram-js repository and the camunda-modeler repository of Camunda. +The bpmn-js repository is licensed under the bpmn.io license and available in the following GitHub repository https://github.com/bpmn-io/bpmn-js. +The diagram-js repository is licensed under the MIT license, available under the following link: https://github.com/bpmn-io/diagram-js. +The camunda-modeler is licensed under the MIT license and available under the following link: https://github.com/camunda/camunda-modeler. Find copies of the third party license in the THIRD_PARTY_LICENSE directory under this directory. \ No newline at end of file diff --git a/README.md b/README.md index a40a9f6e..6987bb3c 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,31 @@ # Quantum Workflow Modeler -A web-based modelling tool for modelling BPMN 2.0-based quantum workflows. It uses and extends the -[bpmn-js Modeler](https://github.com/bpmn-io/bpmn-js/) of Camunda which is embedded in the User Interface (UI) of the -Quantum Workflow Modeler and handles the graphical modelling of workflows. -It contains several modelling extensions to the BPMN standard which enable the modeling of an explicit, executable data -flow and the modelling of quantum specific modelling elements. New model extensions can be integrated plugin-based into -the modeler. Read [this guide](doc/quantum-workflow-modeler/editor/plugin/plugin-integration.md) to learn how you can integrate -your own modelling extensions into the modeler. +[![GitHub license](https://img.shields.io/github/license/PlanQK/workflow-modeler)](https://github.com/PlanQK/workflow-modeler/blob/master/LICENSE) +[![Tests](https://github.com/PlanQK/workflow-modeler/actions/workflows/run-npm-test.yml/badge.svg)](https://github.com/PlanQK/workflow-modeler/actions/workflows/run-npm-test.yml) +[![Release](https://img.shields.io/github/v/release/PlanQK/workflow-modeler)](https://img.shields.io/github/v/release/PlanQK/workflow-modeler) -The modeler is implemented as an HTML web component and can be integrated in other web applications as a custom HMTL tag. -Read [this documentation](doc/integration-guide/integration-guide.md) to learn how you can -integrate the modeler in your application and which configuration options and interfaces you can use. +A web-based modeling tool for modeling BPMN 2.0-based quantum workflows. +It uses and extends the [bpmn-js Modeler](https://github.com/bpmn-io/bpmn-js/) of Camunda which is embedded in the User Interface (UI) of the Quantum Workflow Modeler and handles the graphical modeling of workflows. -The implementation of the modeler is located in the [bpmn-q folder](components/bpmn-q). Example Projects to integrate the -modeler in different UI frameworks can be found [here for a Vue.js app](components/bpmn-q-vue) -and [here for an Angular app](components/bpmn-q-angular). +It contains several modeling extensions to the BPMN standard which enable the modeling of quantum workflows. +New model extensions can be integrated plugin-based into the modeler. +Read [this guide](doc/quantum-workflow-modeler/editor/plugin/plugin-integration.md) to learn how you can integrate your own modeling extensions into the modeler. -The Quantum Workflow Modeler is a HTML web component. -The UI components of the modeler are defined with React-js and written in JavaScript. To package the project, webpack is used. +The modeler is implemented as an HTML web component and can be integrated into other web applications as a custom HTML tag. +Read [this documentation](doc/integration-guide/integration-guide.md) to learn how you can integrate the modeler into your application and which configuration options and interfaces you can use. + +The implementation of the modeler is located in the [bpmn-q folder](components/bpmn-q). +Example projects to integrate the modeler in different UI frameworks can be found [here for a Vue.js app](components/bpmn-q-vue) and [here for an Angular app](components/bpmn-q-angular). + +The Quantum Workflow Modeler is an HTML web component. +The UI components of the modeler are defined with React-js and written in JavaScript. +To package the project, webpack is used. The tests of the project use mocha with chai for karma. Refer to the [documentation](doc/README.md) for further information. ## Node Version + The project was created with npm 8.19.2 and node 18.12.1. ## Quickstart @@ -54,9 +57,8 @@ docker run --name workflow-modeler -p 8080:8080 workflow-modeler ## How to use this Library -To use the Quantum Workflow Modeler component in your application you have to install its npm package which is published -via GitHub packages. To access the package, you first [have to register the PlanQK namespace](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#installing-a-package) -to your npm setup and then [authenticate to GitHub](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token). +To use the Quantum Workflow Modeler component in your application you have to install its npm package which is published via GitHub packages. +To access the package, you first [have to register the PlanQK namespace](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#installing-a-package) to your npm setup and then [authenticate to GitHub](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#authenticating-with-a-personal-access-token). A more detailed description can be found [here](doc/integration-guide/integration-guide.md). Then install the npm package with @@ -64,8 +66,8 @@ Then install the npm package with $> npm install --save @planqk/quantum-workflow-modeler ``` -Register the Quantum Workflow Modeler component as a custom HTML tag in the UI framework you are using. The exact steps -depend on the specific framework, but here are guides how you can do that in [Angular](), [Vue.js]() and [plain HTML](). +Register the Quantum Workflow Modeler component as a custom HTML tag in the UI framework you are using. +The exact steps depend on the specific framework, but here are guides how you can do that in [Angular](), [Vue.js]() and [plain HTML](). Use the tag of the component, ```quantum-workflow-modeler``` directly in your HTML ```html @@ -82,9 +84,9 @@ const modelerComponent = document.createElement('quantum-workflow-modeler'); ``` to integrate the modeler component into the UI of your application. -Activate the plugins you want to use in your instance of the modeler by setting the pluginConfig attribute. You can only -set the pluginConfigs attribute if the modelerComponent is already available in the DOM. If you do not do that, the rendering -will fail. +Activate the plugins you want to use in your instance of the modeler by setting the pluginConfig attribute. +You can only set the pluginConfigs attribute if the modelerComponent is already available in the DOM. +If you do not do that, the rendering will fail. ```javascript modelerComponent.pluginConfigs = [ { @@ -102,23 +104,24 @@ modelerComponent.pluginConfigs = [ ] ``` -You can configure the plugins like described [here](doc/quantum-workflow-modeler/editor/plugin/plugin-config.md). The structure -of the config is defined by the plugin and can be looked up in the documentation of the respective plugin. +You can configure the plugins as described [here](doc/quantum-workflow-modeler/editor/plugin/plugin-config.md). +The structure of the config is defined by the plugin and can be looked up in the documentation of the respective plugin. You can add listeners to custom events the Quantum Workflow Modeler triggers for changes in the currently loaded workflow, like saving or loading a workflow. Read the [EventHandler documentation](doc/quantum-workflow-modeler/editor/events/event-handler-doc.md) to learn more about the events of the modeler. ## Development Setup -To set this project up for development clone the repository and open it in your favorite editor. The project code is under -[./components](components) and is split in three parts: Under [bpmn-q](components/bpmn-q) is the actual code of the Quantum -Workflow Modeler. Under [bpmn-q-angular](components/bpmn-q-angular) is an example project with Angular which integrates the -modeler component. Under [bpmn-q-vue](components/bpmn-q-vue) is a simple Vue-js project which integrate the modeler. These -to projects do not contain code of the Quantum Workflow Modeler component. They are used for testing the integration of the -developed component to test and check its compatibility with other UI frameworks. +To set this project up for development, clone the repository and open it in your favorite editor. +The project code is under [./components](components) and is split in three parts: +Under [bpmn-q](components/bpmn-q) is the actual code of the Quantum Workflow Modeler. +Under [bpmn-q-angular](components/bpmn-q-angular) is an example project with Angular which integrates the modeler component. +Under [bpmn-q-vue](components/bpmn-q-vue) is a simple Vue-js project which integrate the modeler. +These to projects do not contain code of the Quantum Workflow Modeler component. +They are used for testing the integration of the developed component to test and check its compatibility with other UI frameworks. -The actual code for development is in [bpmn-q](components/bpmn-q). To set up the cloned project, execute the following -commands under the ./components/bpmn-q directory. +The actual code for development is in [bpmn-q](components/bpmn-q). +To set up the cloned project, execute the following commands under the ./components/bpmn-q directory. 1. Install dependencies ``` npm install @@ -127,7 +130,7 @@ commands under the ./components/bpmn-q directory. 2. Start the Modeler To execute the Quantum Workflow Modeler, a small test website can be run which only contains the modeler component. - To start this website, execute + To start this website, execute: ``` npm run dev ``` @@ -135,7 +138,7 @@ commands under the ./components/bpmn-q directory. 3. Build the Modeler - To build the modeler execute + To build the modeler execute: ``` npm run build ``` @@ -143,7 +146,7 @@ commands under the ./components/bpmn-q directory. 4. Run all Tests - To execute all tests run + To execute all tests run: ``` npm test ``` @@ -155,16 +158,10 @@ commands under the ./components/bpmn-q directory. ## Disclaimer of Warranty -Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its -Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, -without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR -PURPOSE. -You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks -associated with Your exercise of permissions under this License. +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. +You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. ## Haftungsausschluss Dies ist ein Forschungsprototyp. -Die Haftung für entgangenen Gewinn, Produktionsausfall, Betriebsunterbrechung, entgangene Nutzungen, Verlust von Daten -und Informationen, Finanzierungsaufwendungen sowie sonstige Vermögens- und Folgeschäden ist, außer in Fällen von grober -Fahrlässigkeit, Vorsatz und Personenschäden, ausgeschlossen. +Die Haftung für entgangenen Gewinn, Produktionsausfall, Betriebsunterbrechung, entgangene Nutzungen, Verlust von Daten und Informationen, Finanzierungsaufwendungen sowie sonstige Vermögens- und Folgeschäden ist, außer in Fällen von grober Fahrlässigkeit, Vorsatz und Personenschäden, ausgeschlossen. diff --git a/components/bpmn-q/.eslintignore b/components/bpmn-q/.eslintignore new file mode 100644 index 00000000..d70ebaa1 --- /dev/null +++ b/components/bpmn-q/.eslintignore @@ -0,0 +1 @@ +public \ No newline at end of file diff --git a/components/bpmn-q/.eslintrc.js b/components/bpmn-q/.eslintrc.js deleted file mode 100644 index ee100eb0..00000000 --- a/components/bpmn-q/.eslintrc.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = { - "env": { - "browser": true, - "es2021": true, - "commonjs": true, - "node": true, - "mocha": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "overrides": [ - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module", - }, - "plugins": [ - "react" - ], - "rules": { - "semi": [2, "always"], - "react/prop-types": "off", - "no-prototype-builtins": "off", - "react/jsx-key": "off", - "no-unused-vars": "off", - "no-useless-escape": "off", - } -}; diff --git a/components/bpmn-q/.eslintrc.json b/components/bpmn-q/.eslintrc.json new file mode 100644 index 00000000..3fbe275d --- /dev/null +++ b/components/bpmn-q/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "env": { + "browser": true, + "es2021": true, + "commonjs": true, + "node": true, + "mocha": true + }, + "extends": ["eslint:recommended", "plugin:react/recommended"], + "overrides": [], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react"], + "rules": { + "semi": [2, "always"], + "react/prop-types": "off", + "no-prototype-builtins": "off", + "react/jsx-key": "off" + } +} diff --git a/components/bpmn-q/.prettierignore b/components/bpmn-q/.prettierignore new file mode 100644 index 00000000..79b158cb Binary files /dev/null and b/components/bpmn-q/.prettierignore differ diff --git a/components/bpmn-q/.prettierrc.json b/components/bpmn-q/.prettierrc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/components/bpmn-q/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/components/bpmn-q/README.md b/components/bpmn-q/README.md index 4b89baad..a7987a45 100644 --- a/components/bpmn-q/README.md +++ b/components/bpmn-q/README.md @@ -1,15 +1,19 @@ # Quantum Workflow Modeler - HTML Web Component -This project contains the HTML web component for the Quantum Workflow Modeler and its implementation. + +This project contains the HTML web component for the Quantum Workflow Modeler and its implementation. ## Node Version + The project was created with npm 8.19.2 and node 18.12.1. ## Development Setup -To set this project up for development clone the repository and open it in your favorite editor. +To set this project up for development clone the repository and open it in your favorite editor. Execute the following commands under this directory: + ### Install dependencies + ``` npm install ``` @@ -18,30 +22,37 @@ npm install To execute the Quantum Workflow Modeler, a small test website can be run which only contains the modeler component. To start this website, execute + ``` npm run dev ``` + This will start a webpack dev server which loads the website specified in the [index.html file](components/bpmn-q/public/index.html) ### Build the Modeler To build the modeler execute + ``` npm run build ``` + This will build the modeler component with webpack into a single js file in the [public directory](components/bpmn-q/public). ### Run all Tests To execute all tests run + ``` -npm test +npm test ``` - This will run all mocha test specified in [karma.conf.js](components/bpmn-q/karma.conf.js) with karma. + +This will run all mocha test specified in [karma.conf.js](components/bpmn-q/karma.conf.js) with karma. ### External Endpoints - Some components of the modeler component need external endpoints to work properly. Refer to [this guide](doc/devloper-setup/developer-setup.md) - for setting up all used endpoints. + +Some components of the modeler component need external endpoints to work properly. Refer to [this guide](doc/devloper-setup/developer-setup.md) +for setting up all used endpoints. ## Disclaimer of Warranty @@ -57,4 +68,4 @@ associated with Your exercise of permissions under this License. Dies ist ein Forschungsprototyp. Die Haftung für entgangenen Gewinn, Produktionsausfall, Betriebsunterbrechung, entgangene Nutzungen, Verlust von Daten und Informationen, Finanzierungsaufwendungen sowie sonstige Vermögens- und Folgeschäden ist, außer in Fällen von grober -Fahrlässigkeit, Vorsatz und Personenschäden, ausgeschlossen. \ No newline at end of file +Fahrlässigkeit, Vorsatz und Personenschäden, ausgeschlossen. diff --git a/components/bpmn-q/babel.config.js b/components/bpmn-q/babel.config.js index c4b9486f..1a58db7b 100644 --- a/components/bpmn-q/babel.config.js +++ b/components/bpmn-q/babel.config.js @@ -1,31 +1,31 @@ module.exports = { - presets: [ + presets: [ + [ + "@babel/preset-env", + { + modules: false, + }, + ], + "@babel/preset-react", + ], + plugins: [ + "@babel/plugin-transform-runtime", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties", + ], + env: { + production: { + only: ["src"], + plugins: [ [ - "@babel/preset-env", - { - modules: false - } + "transform-react-remove-prop-types", + { + removeImport: true, + }, ], - "@babel/preset-react" - ], - plugins: [ - "@babel/plugin-transform-runtime", - "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-proposal-class-properties" - ], - env: { - production: { - only: ["src"], - plugins: [ - [ - "transform-react-remove-prop-types", - { - removeImport: true - } - ], - "@babel/plugin-transform-react-inline-elements", - "@babel/plugin-transform-react-constant-elements" - ] - } - } -}; \ No newline at end of file + "@babel/plugin-transform-react-inline-elements", + "@babel/plugin-transform-react-constant-elements", + ], + }, + }, +}; diff --git a/components/bpmn-q/bpmnlint-plugin-custom/index.js b/components/bpmn-q/bpmnlint-plugin-custom/index.js index 450b234d..f4c2403d 100644 --- a/components/bpmn-q/bpmnlint-plugin-custom/index.js +++ b/components/bpmn-q/bpmnlint-plugin-custom/index.js @@ -1,15 +1,14 @@ module.exports = { configs: { recommended: { - rules: { - } + rules: {}, }, all: { rules: { - 'quantme-tasks': 'warn', - 'subprocess-required-start-event': 'warn', - 'subprocess-connected-end-event': 'warn' - } - } - } -} \ No newline at end of file + "quantme-tasks": "warn", + "subprocess-required-start-event": "warn", + "subprocess-connected-end-event": "warn", + }, + }, + }, +}; diff --git a/components/bpmn-q/bpmnlint-plugin-custom/rules/quantme-tasks.js b/components/bpmn-q/bpmnlint-plugin-custom/rules/quantme-tasks.js index b986503d..24c6a3b1 100644 --- a/components/bpmn-q/bpmnlint-plugin-custom/rules/quantme-tasks.js +++ b/components/bpmn-q/bpmnlint-plugin-custom/rules/quantme-tasks.js @@ -1,20 +1,22 @@ -let QuantMEAttributeChecker = require('../../modeler-component/extensions/quantme/replacement/QuantMEAttributeChecker'); +let QuantMEAttributeChecker = require("../../modeler-component/extensions/quantme/replacement/QuantMEAttributeChecker"); /** * Rule that reports QuantME tasks for which no suited replacement model exists */ -module.exports = function() { - +module.exports = function () { function check(node, reporter) { - if (node.$type && node.$type.startsWith('quantme:')) { + if (node.$type && node.$type.startsWith("quantme:")) { if (!QuantMEAttributeChecker.requiredAttributesAvailable(node)) { - reporter.report(node.id, 'Not all required attributes are set. Unable to replace task!'); + reporter.report( + node.id, + "Not all required attributes are set. Unable to replace task!" + ); return; } } } return { - check: check + check: check, }; -}; \ No newline at end of file +}; diff --git a/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-connected-end-event.js b/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-connected-end-event.js index d7f2740e..3e50dc44 100644 --- a/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-connected-end-event.js +++ b/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-connected-end-event.js @@ -1,46 +1,41 @@ -const { - is, - isAny -} = require('bpmnlint-utils'); - +const { is, isAny } = require("bpmnlint-utils"); /** * A rule that checks the presence of an end event per scope. */ module.exports = function () { - function hasEndEvent(node) { const flowElements = node.flowElements || []; - return ( - flowElements.some(node => is(node, 'bpmn:EndEvent')) - ); + return flowElements.some((node) => is(node, "bpmn:EndEvent")); } function hasConnectedEndEvent(node) { const flowElements = node.flowElements || []; - return ( - flowElements.some(node => { const incomingflow = node.incoming || []; return is(node, 'bpmn:EndEvent') && incomingflow.length === 0; }) - ); - + return flowElements.some((node) => { + const incomingflow = node.incoming || []; + return is(node, "bpmn:EndEvent") && incomingflow.length === 0; + }); } function check(node, reporter) { - console.log(node) - if (!isAny(node, ['bpmn:SubProcess'])) { + console.log(node); + if (!isAny(node, ["bpmn:SubProcess"])) { return; } if (!hasEndEvent(node)) { - - reporter.report(node.id, 'Subprocess is missing end event'); + reporter.report(node.id, "Subprocess is missing end event"); } if (hasConnectedEndEvent(node)) { - reporter.report(node.id, 'Each end event must have at least one incoming flow'); + reporter.report( + node.id, + "Each end event must have at least one incoming flow" + ); } } return { check }; -}; \ No newline at end of file +}; diff --git a/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-required-start-event.js b/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-required-start-event.js index bbb06d17..5d83043a 100644 --- a/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-required-start-event.js +++ b/components/bpmn-q/bpmnlint-plugin-custom/rules/subprocess-required-start-event.js @@ -1,38 +1,33 @@ -const { - is -} = require('bpmnlint-utils'); - +const { is } = require("bpmnlint-utils"); /** * A rule that checks that start events inside a normal sub-processes * are blank (do not have an event definition). */ module.exports = function () { - function check(node, reporter) { - - if (!is(node, 'bpmn:SubProcess') || node.triggeredByEvent) { + if (!is(node, "bpmn:SubProcess") || node.triggeredByEvent) { return; } const flowElements = node.flowElements || []; flowElements.forEach(function (flowElement) { - - if (!is(flowElement, 'bpmn:StartEvent')) { + if (!is(flowElement, "bpmn:StartEvent")) { return false; } const eventDefinitions = flowElement.eventDefinitions || []; if (eventDefinitions.length > 0) { - reporter.report(flowElement.id, 'Start event must be blank', ['eventDefinitions']); + reporter.report(flowElement.id, "Start event must be blank", [ + "eventDefinitions", + ]); } }); } return { - check + check, }; - -}; \ No newline at end of file +}; diff --git a/components/bpmn-q/client/index.js b/components/bpmn-q/client/index.js index 01847a26..22aeb18d 100644 --- a/components/bpmn-q/client/index.js +++ b/components/bpmn-q/client/index.js @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Institute of Architecture of Application Systems - + * Copyright (c) 2023 Institute of Architecture of Application Systems - * University of Stuttgart * * This program and the accompanying materials are made available under the @@ -9,14 +9,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { registerBpmnJSPlugin } from 'camunda-modeler-plugin-helpers'; +import { registerBpmnJSPlugin } from "camunda-modeler-plugin-helpers"; -import customLinterConfig from '../.bpmnlintrc'; +import customLinterConfig from "../.bpmnlintrc"; registerBpmnJSPlugin({ __init__: [ - function(linting) { + function (linting) { linting.setLinterConfig(customLinterConfig); - } - ] + }, + ], }); diff --git a/components/bpmn-q/karma.conf.js b/components/bpmn-q/karma.conf.js index ebf95197..27a9c23b 100644 --- a/components/bpmn-q/karma.conf.js +++ b/components/bpmn-q/karma.conf.js @@ -1,66 +1,65 @@ // Karma configuration -const webpackConfig = require('./webpack.config.js'); +const webpackConfig = require("./webpack.config.js"); module.exports = function (config) { - config.set({ + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "", - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '', + // frameworks to use + // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter + frameworks: ["mocha", "webpack"], - // frameworks to use - // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter - frameworks: ['mocha', 'webpack'], + // list of files / patterns to load in the browser + files: [ + "test/tests/editor/configurations.spec.js", + "test/tests/editor/editor.spec.js", + "test/tests/editor/plugin.spec.js", + "test/tests/planqk/planqk-transformation.spec.js", + "test/tests/editor/utils/modelling-util.spec.js", + "test/tests/qhana/qhana-plugin-config.spec.js", + "test/tests/qhana/qhana-service-configs.spec.js", + "test/tests/quantme/quantme-transformation.spec.js", + "test/tests/quantme/data-object-configs.spec.js", + "test/tests/quantme/quantme-config.spec.js", + "test/tests/opentosca/opentosca-config.spec.js", + "test/tests/opentosca/deployment-utils.spec.js", + "test/tests/opentosca/deployment-model-renderer.spec.js", + "test/tests/dataflow/data-flow-transformation.spec.js", + "test/tests/dataflow/data-flow-plugin-config.spec.js", + "test/tests/dataflow/data-flow-configurations-endpoint.spec.js", + "test/tests/dataflow/data-flow-palette.spec.js", + "test/tests/dataflow/data-flow-replace-menu.spec.js", + ], - // list of files / patterns to load in the browser - files: [ - 'test/tests/editor/configurations.spec.js', - 'test/tests/editor/editor.spec.js', - 'test/tests/editor/plugin.spec.js', - 'test/tests/planqk/planqk-transformation.spec.js', - 'test/tests/editor/utils/modelling-util.spec.js', - 'test/tests/qhana/qhana-plugin-config.spec.js', - 'test/tests/qhana/qhana-service-configs.spec.js', - 'test/tests/quantme/quantme-transformation.spec.js', - 'test/tests/quantme/data-object-configs.spec.js', - 'test/tests/quantme/quantme-config.spec.js', - 'test/tests/opentosca/opentosca-config.spec.js', - 'test/tests/opentosca/deployment-utils.spec.js', - 'test/tests/opentosca/deployment-model-renderer.spec.js', - 'test/tests/dataflow/data-flow-transformation.spec.js', - 'test/tests/dataflow/data-flow-plugin-config.spec.js', - 'test/tests/dataflow/data-flow-configurations-endpoint.spec.js', - 'test/tests/dataflow/data-flow-palette.spec.js', - 'test/tests/dataflow/data-flow-replace-menu.spec.js', - ], + // list of files / patterns to exclude + exclude: [], - // list of files / patterns to exclude - exclude: [], + // preprocess matching files before serving them to the browser + // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor + preprocessors: { + "test/**/*.spec.js": ["webpack"], + }, - // preprocess matching files before serving them to the browser - // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor - preprocessors: { - 'test/**/*.spec.js': ['webpack'] - }, + webpack: webpackConfig, - webpack: webpackConfig, + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, + // start these browsers + // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher + browsers: ["ChromeHeadless"], - // start these browsers - // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher - browsers: ['ChromeHeadless'], + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: true, + // Concurrency level + // how many browser instances should be started simultaneously + concurrency: 1, - // Concurrency level - // how many browser instances should be started simultaneously - concurrency: 1, - - mochaReporter: { - output: "minimal" - }, - }); -}; \ No newline at end of file + mochaReporter: { + output: "minimal", + }, + }); +}; diff --git a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js index 35728c0d..d923a455 100644 --- a/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js +++ b/components/bpmn-q/modeler-component/QuantumWorkflowModeler.js @@ -1,25 +1,36 @@ -import 'bpmn-js/dist/assets/diagram-js.css'; -import 'bpmn-js-properties-panel/dist/assets/element-templates.css'; -import 'bpmn-js-properties-panel/dist/assets/properties-panel.css'; -import './editor/resources/styling/modeler.css'; -import './editor/resources/styling/editor-ui.css'; -import './editor/ui/notifications/Notifications.css'; -import './editor/ui/notifications/Notification.css'; -import './editor/resources/styling/camunda-styles/style.css'; -import 'bpmn-js-bpmnlint/dist/assets/css/bpmn-js-bpmnlint.css'; -import './modeler.css'; - -import React from 'react'; -import { createRoot } from 'react-dom/client'; +import "bpmn-js/dist/assets/diagram-js.css"; +import "bpmn-js-properties-panel/dist/assets/element-templates.css"; +import "bpmn-js-properties-panel/dist/assets/properties-panel.css"; +import "./editor/resources/styling/modeler.css"; +import "./editor/resources/styling/editor-ui.css"; +import "./editor/ui/notifications/Notifications.css"; +import "./editor/ui/notifications/Notification.css"; +import "./editor/resources/styling/camunda-styles/style.css"; +import "bpmn-js-bpmnlint/dist/assets/css/bpmn-js-bpmnlint.css"; +import "./modeler.css"; + +import React from "react"; +import { createRoot } from "react-dom/client"; import ButtonToolbar from "./editor/ui/ButtonToolbar"; -import { createNewDiagram, loadDiagram, setAutoSaveInterval } from "./editor/util/IoUtilities"; +import { + createNewDiagram, + loadDiagram, + setAutoSaveInterval, +} from "./editor/util/IoUtilities"; import NotificationHandler from "./editor/ui/notifications/NotificationHandler"; import { createModeler, getModeler } from "./editor/ModelerHandler"; -import { getPluginButtons, getTransformationButtons } from "./editor/plugin/PluginHandler"; -import { getPluginConfig, setPluginConfig } from "./editor/plugin/PluginConfigHandler"; -import * as editorConfig from './editor/config/EditorConfigManager'; -import { initEditorEventHandler } from './editor/events/EditorEventHandler'; -import $ from 'jquery'; +import { + getPluginButtons, + getTransformationButtons, +} from "./editor/plugin/PluginHandler"; +import { + getPluginConfig, + setPluginConfig, +} from "./editor/plugin/PluginConfigHandler"; +import * as editorConfig from "./editor/config/EditorConfigManager"; +import { initEditorEventHandler } from "./editor/events/EditorEventHandler"; +import $ from "jquery"; +import { edit } from "ace-builds"; /** * The Quantum Workflow modeler HTML web component which contains the bpmn-js modeler to model BPMN diagrams, an editor @@ -27,54 +38,53 @@ import $ from 'jquery'; * the modelling of quantum workflows. */ export class QuantumWorkflowModeler extends HTMLElement { - - workflowModel; - constructor() { - super(); - } - - connectedCallback() { - - // create the HTML structure of the component - this.setInnerHtml(); - - // add listener for post messages containing a workflow to load into the modeler - const self = this; - window.addEventListener("message", function (event) { - - // check if the message contains a correctly formatted workflow - if (event.origin === window.location.href.replace(/\/$/, '') - && event.data && event.data.workflow && typeof event.data.workflow === 'string' && event.data.workflow.startsWith('')) { - - const xmlString = event.data.workflow; - self.workflowModel = xmlString; - - // open sent workflow and save its file name - editorConfig.setFileName(event.data.name); - loadDiagram(xmlString, getModeler()).then(); - } - }); - - // wait until shadow dom is loaded - requestAnimationFrame(() => { - - // start the bpmn-js modeler and render the React components - this.startModeler(); - }); - - const beforeUnloadListener = (event) => { - event.preventDefault(); - return event.returnValue = ''; - }; - addEventListener("beforeunload", beforeUnloadListener, { capture: true }); - } - - - /** - * Set up the inner structure of the component - */ - setInnerHtml() { - this.innerHTML = ` + workflowModel; + constructor() { + super(); + } + + connectedCallback() { + // create the HTML structure of the component + this.setInnerHtml(); + + // add listener for post messages containing a workflow to load into the modeler + const self = this; + window.addEventListener("message", function (event) { + // check if the message contains a correctly formatted workflow + if ( + event.origin === window.location.href.replace(/\/$/, "") && + event.data && + event.data.workflow && + typeof event.data.workflow === "string" && + event.data.workflow.startsWith('') + ) { + const xmlString = event.data.workflow; + self.workflowModel = xmlString; + + // open sent workflow and save its file name + editorConfig.setFileName(event.data.name); + loadDiagram(xmlString, getModeler()).then(); + } + }); + + // wait until shadow dom is loaded + requestAnimationFrame(() => { + // start the bpmn-js modeler and render the React components + this.startModeler(); + }); + + const beforeUnloadListener = (event) => { + event.preventDefault(); + return (event.returnValue = ""); + }; + addEventListener("beforeunload", beforeUnloadListener, { capture: true }); + } + + /** + * Set up the inner structure of the component + */ + setInnerHtml() { + this.innerHTML = `

@@ -86,259 +96,271 @@ export class QuantumWorkflowModeler extends HTMLElement {
`; - let panel = document.getElementById("properties"); - let maindiv = document.getElementById("main-div"); - - let isResizing = false; - let startX; - let startWidth; - let width = panel.style.width; - let propertiesElement = document.getElementById("properties"); - - propertiesElement.addEventListener("mousemove", function (e) { - let rect = this.getBoundingClientRect(); - let x = e.clientX - rect.left; - let y = e.clientY - rect.top; - - let borderSize = 5; - - if ( - x < borderSize || - x > rect.width - borderSize || - y < borderSize || - y > rect.height - borderSize - ) { - this.style.cursor = "w-resize"; - } else { - this.style.cursor = "default"; - - } - }); - - - // Mouse down event listener - panel.addEventListener('mousedown', handleMouseDown); - - panel.addEventListener("mouseup", function () { - this.style.cursor = "default"; - }); - - // Mouse move event listener - document.addEventListener('mousemove', handleMouseMove); - - // Mouse up event listener - document.addEventListener('mouseup', handleMouseUp); - - // Mouse down handler - function handleMouseDown(event) { - let rect = panel.getBoundingClientRect(); - let x = event.clientX - rect.left; - - let borderSize = 5; + let panel = document.getElementById("properties"); + let maindiv = document.getElementById("main-div"); + + let isResizing = false; + let startX; + let startWidth; + let width = panel.style.width; + let propertiesElement = document.getElementById("properties"); + + propertiesElement.addEventListener("mousemove", function (e) { + let rect = this.getBoundingClientRect(); + let x = e.clientX - rect.left; + let y = e.clientY - rect.top; + + let borderSize = 5; + + if ( + x < borderSize || + x > rect.width - borderSize || + y < borderSize || + y > rect.height - borderSize + ) { + this.style.cursor = "w-resize"; + } else { + this.style.cursor = "default"; + } + }); + + // Mouse down event listener + panel.addEventListener("mousedown", handleMouseDown); + + panel.addEventListener("mouseup", function () { + this.style.cursor = "default"; + }); + + // Mouse move event listener + document.addEventListener("mousemove", handleMouseMove); + + // Mouse up event listener + document.addEventListener("mouseup", handleMouseUp); + + // Mouse down handler + function handleMouseDown(event) { + let rect = panel.getBoundingClientRect(); + let x = event.clientX - rect.left; + + let borderSize = 5; + + if (x < borderSize || x > rect.width - borderSize) { + isResizing = true; + } + startX = event.clientX; + startWidth = parseFloat(panel.style.width); + } + let isCollapsed = false; + const resizeButton = document.createElement("button"); + resizeButton.className = "fa fa-angle-right resize"; + maindiv.appendChild(resizeButton); + + // Mouse move handler + function handleMouseMove(event) { + if (!isResizing) { + maindiv.style.cursor = "default"; + return; + } + maindiv.style.cursor = "w-resize"; + panel.style.cursor = "w-resize"; + const deltaX = event.clientX - startX; + let newWidth = startWidth - deltaX; + + // enable to completely hide the panel + if (newWidth < 20) { + newWidth = 0; + isCollapsed = true; + resizeButton.className = "fa fa-angle-left resize"; + } + panel.style.width = `${newWidth}px`; + } - if ( - x < borderSize || - x > rect.width - borderSize - ) { + // Mouse up handler + function handleMouseUp() { + panel.style.cursor = "default"; + isResizing = false; + } - isResizing = true; - } - startX = event.clientX; - startWidth = parseFloat(panel.style.width); + resizeButton.addEventListener("click", function () { + let offsetWidth = panel.offsetWidth; + if (isCollapsed) { + panel.style.display = "block"; + panel.style.width = offsetWidth; + if (panel.offsetWidth < parseInt(width, 10)) { + panel.style.width = width; } - let isCollapsed = false; - const resizeButton = document.createElement('button'); resizeButton.className = "fa fa-angle-right resize"; - maindiv.appendChild(resizeButton); - - // Mouse move handler - function handleMouseMove(event) { - if (!isResizing) { maindiv.style.cursor = "default"; return; } - maindiv.style.cursor = "w-resize"; - panel.style.cursor = "w-resize"; - const deltaX = event.clientX - startX; - let newWidth = startWidth - deltaX; - - // enable to completely hide the panel - if (newWidth < 20) { - newWidth = 0; - isCollapsed = true; - resizeButton.className = "fa fa-angle-left resize"; - } - panel.style.width = `${newWidth}px`; - } - - // Mouse up handler - function handleMouseUp() { - panel.style.cursor = "default"; - isResizing = false; + } else { + panel.style.display = "none"; + resizeButton.className = "fa fa-angle-left resize"; + } + + isCollapsed = !isCollapsed; + }); + + let editor = document.getElementById("editor"); + let dragging = false; + let aceEditor = edit(editor); + + $("#editor_dragbar").mousedown(function (e) { + e.preventDefault(); + dragging = true; + + let editorElement = $("#editor"); + let editor_wrap = $("#editor_wrap"); + let dragbar = $("#editor_dragbar"); + let startY = e.pageY; + let startTop = parseInt(editorElement.css("top")); + let startHeight = editor_wrap.height(); + + $(document).mousemove(function (e) { + if (!dragging) return; + + let actualY = e.pageY; + let deltaY = startY - actualY; + let newTop = startTop - deltaY; + let newHeight = startHeight + deltaY; + const viewportHeight = window.innerHeight; + const heightInVh = (newHeight / viewportHeight) * 100; + + // since we move the editor element up we need to add the actual height of the + // wrapper element + const editorHeight = 2 * newHeight; + if (newHeight >= 75 && heightInVh <= 89) { + editorElement.css("top", newTop + "px"); + editor_wrap.css("height", newHeight + "px"); + editorElement.css("height", editorHeight + "px"); + dragbar.css("top", newTop - dragbar.height() + "px"); + aceEditor.resize(); } - - - resizeButton.addEventListener('click', function () { - let offsetWidth = panel.offsetWidth; - if (isCollapsed) { - panel.style.display = 'block'; - panel.style.width = offsetWidth; - if (panel.offsetWidth < parseInt(width, 10)) { - panel.style.width = width; - } - resizeButton.className = "fa fa-angle-right resize"; - } else { - panel.style.display = 'none'; - resizeButton.className = "fa fa-angle-left resize"; - } - - isCollapsed = !isCollapsed; - }); - - let editor = document.getElementById('editor'); - let dragging = false; - let aceEditor = ace.edit(editor); - - - $("#editor_dragbar").mousedown(function (e) { - e.preventDefault(); - dragging = true; - - let editorElement = $("#editor"); - let editor_wrap = $("#editor_wrap"); - let dragbar = $("#editor_dragbar"); - let startY = e.pageY; - let startTop = parseInt(editorElement.css("top")); - let startHeight = editor_wrap.height(); - - $(document).mousemove(function (e) { - if (!dragging) return; - - let actualY = e.pageY; - let deltaY = startY - actualY; - let newTop = startTop - deltaY; - let newHeight = startHeight + deltaY; - const viewportHeight = window.innerHeight; - const heightInVh = (newHeight / viewportHeight) * 100; - - // since we move the editor element up we need to add the actual height of the - // wrapper element - const editorHeight = 2 * newHeight; - if (newHeight >= 75 && heightInVh <= 89) { - editorElement.css("top", newTop + "px"); - editor_wrap.css("height", newHeight + "px"); - editorElement.css("height", editorHeight + "px"); - dragbar.css("top", newTop - dragbar.height() + "px"); - aceEditor.resize(); - } - }); - }); - - $(document).mouseup(function (e) { - if (dragging) { - dragging = false; - $(document).unbind("mousemove"); - } + }); + }); + + $(document).mouseup(function () { + if (dragging) { + dragging = false; + $(document).unbind("mousemove"); + } + }); + } + + /** + * Initializes the modeler component by creating the bpmn-js modeler instance and rendering the React components of + * the editor into the DOM. + */ + startModeler() { + console.log("Start Modeler"); + + // initialize event handler for workflow events with the instance of the component to dispatch the events correctly + initEditorEventHandler(this); + + // get and reset the container in which the bpmn-js modeler and its properties panel should be rendered + const bpmnContainer = document.getElementById("canvas"); + const propertiesPanelContainer = document.getElementById("properties"); + bpmnContainer.innerHTML = ""; + propertiesPanelContainer.innerHTML = ""; + + // create a new bpmn-js modeler instance with all additional modules and extensions defined by the plugins + const modeler = createModeler(bpmnContainer, propertiesPanelContainer); + console.log("Created Modeler"); + + // set up the notification handler and render it into the DOM + const notificationsContainer = document.getElementById( + "qwm-notification-container" + ); + const handler = NotificationHandler.getInstance(); + const notificationComponent = handler.createNotificationsComponent( + [], + notificationsContainer + ); + + const notificationRoot = createRoot(notificationsContainer); + notificationRoot.render(
{notificationComponent}
); + console.log("Rendered Notifications React Component"); + + // create a transformation button for each transformation method of an active plugin + const transformationButtons = getTransformationButtons(); + + // integrate the React ButtonToolbar into its DOM container + const root = createRoot(document.getElementById("button-container")); + root.render( + + ); + + // load initial workflow + this.workflowModel = + this.workflowModel || getPluginConfig("editor").defaultWorkflow; + getModeler().on("commandStack.changed", function () { + getModeler() + .saveXML({ format: true }) + .then(function (result) { + modeler.xml = result; }); + }); + if (this.workflowModel) { + loadDiagram(this.workflowModel, getModeler()).then(); + } else { + createNewDiagram(modeler); } - - /** - * Initializes the modeler component by creating the bpmn-js modeler instance and rendering the React components of - * the editor into the DOM. - */ - startModeler() { - console.log('Start Modeler'); - - // initialize event handler for workflow events with the instance of the component to dispatch the events correctly - initEditorEventHandler(this); - - // get and reset the container in which the bpmn-js modeler and its properties panel should be rendered - const bpmnContainer = document.getElementById('canvas'); - const propertiesPanelContainer = document.getElementById('properties'); - bpmnContainer.innerHTML = ''; - propertiesPanelContainer.innerHTML = ''; - - // create a new bpmn-js modeler instance with all additional modules and extensions defined by the plugins - const modeler = createModeler(bpmnContainer, propertiesPanelContainer); - console.log('Created Modeler'); - - // set up the notification handler and render it into the DOM - const notificationsContainer = document.getElementById('qwm-notification-container'); - const handler = NotificationHandler.getInstance(); - const notificationComponent = handler.createNotificationsComponent([], notificationsContainer); - - const notificationRoot = createRoot(notificationsContainer); - notificationRoot.render(
{notificationComponent}
); - console.log('Rendered Notifications React Component'); - - // create a transformation button for each transformation method of an active plugin - const transformationButtons = getTransformationButtons(); - - // integrate the React ButtonToolbar into its DOM container - const root = createRoot(document.getElementById('button-container')); - root.render(); - - // load initial workflow - this.workflowModel = this.workflowModel || getPluginConfig('editor').defaultWorkflow; - getModeler().on('commandStack.changed', function () { - getModeler().saveXML({ format: true }).then(function (result) { - modeler.xml = result; - }) - }); - if (this.workflowModel) { - loadDiagram(this.workflowModel, getModeler()).then(); - } else { - createNewDiagram(modeler); - } - } - - /** - * Load the given xml string as a workflow into the modeler. - * - * @param xmlDiagram The workflow to load as xml string - * @return {Promise<*|undefined>} - */ - async loadWorkflowDiagram(xmlDiagram) { - const modeler = getModeler(); - - if (modeler) { - return await loadDiagram(xmlDiagram, getModeler()); - } else { - console.log('Loading of Workflow via external interface not possible until modeler is loaded.'); - } - - } - - /** - * Getter for the plugin config of the Quantum Workflow Modeler - * - * @return {*[]} The plugin config as an array of {name: string, (optional) config: {}} - */ - get pluginConfigs() { - return this.pluginConfigsList || []; - } - - /** - * Setter for the plugin config of the Quantum Workflow Modeler - * - * @param pluginConfigs The plugin config as an array of {name: string, (optional) config: {}} - */ - set pluginConfigs(pluginConfigs) { - console.log(pluginConfigs); - this.pluginConfigsList = pluginConfigs; - const configs = this.pluginConfigsList; - console.log(configs); - - // add plugin config to the PluginConfigHandler - setPluginConfig(configs); - - // rerender shadow dom to add plugin elements - this.setInnerHtml(); - - // restart modeler to apply plugin config when shadow dom is rendered - requestAnimationFrame(() => { - this.startModeler(); - setAutoSaveInterval(); - }); + } + + /** + * Load the given xml string as a workflow into the modeler. + * + * @param xmlDiagram The workflow to load as xml string + * @return {Promise<*|undefined>} + */ + async loadWorkflowDiagram(xmlDiagram) { + const modeler = getModeler(); + + if (modeler) { + return await loadDiagram(xmlDiagram, getModeler()); + } else { + console.log( + "Loading of Workflow via external interface not possible until modeler is loaded." + ); } + } + + /** + * Getter for the plugin config of the Quantum Workflow Modeler + * + * @return {*[]} The plugin config as an array of {name: string, (optional) config: {}} + */ + get pluginConfigs() { + return this.pluginConfigsList || []; + } + + /** + * Setter for the plugin config of the Quantum Workflow Modeler + * + * @param pluginConfigs The plugin config as an array of {name: string, (optional) config: {}} + */ + set pluginConfigs(pluginConfigs) { + console.log(pluginConfigs); + this.pluginConfigsList = pluginConfigs; + const configs = this.pluginConfigsList; + console.log(configs); + + // add plugin config to the PluginConfigHandler + setPluginConfig(configs); + + // rerender shadow dom to add plugin elements + this.setInnerHtml(); + + // restart modeler to apply plugin config when shadow dom is rendered + requestAnimationFrame(() => { + this.startModeler(); + setAutoSaveInterval(); + }); + } } -window.customElements.define('quantum-workflow-modeler', QuantumWorkflowModeler); +window.customElements.define( + "quantum-workflow-modeler", + QuantumWorkflowModeler +); diff --git a/components/bpmn-q/modeler-component/editor/EditorConstants.js b/components/bpmn-q/modeler-component/editor/EditorConstants.js index 288375b2..46bcdefb 100644 --- a/components/bpmn-q/modeler-component/editor/EditorConstants.js +++ b/components/bpmn-q/modeler-component/editor/EditorConstants.js @@ -1,27 +1,26 @@ - // supported options to handle a transformed workflow export const transformedWorkflowHandlers = { - NEW_TAB: 'Open in new Tab', - SAVE_AS_FILE: 'Save as File' + NEW_TAB: "Open in new Tab", + SAVE_AS_FILE: "Save as File", }; // workflow event types dispatched by the EditorEventHandler export const workflowEventTypes = { - LOADED: 'quantum-workflow-loaded', // New Workflow loaded in modeler - SAVED: 'quantum-workflow-saved', // Workflow saved - TRANSFORMED: 'quantum-workflow-transformed', // Workflow transformed - DEPLOYED: 'quantum-workflow-deployed', // Workflow deployed to workflow engine + LOADED: "quantum-workflow-loaded", // New Workflow loaded in modeler + SAVED: "quantum-workflow-saved", // Workflow saved + TRANSFORMED: "quantum-workflow-transformed", // Workflow transformed + DEPLOYED: "quantum-workflow-deployed", // Workflow deployed to workflow engine }; export const autoSaveFile = { - INTERVAL: 'Interval', - ON_ACTION: 'On Action' -} + INTERVAL: "Interval", + ON_ACTION: "On Action", +}; // supported save file options export const saveFileFormats = { - ALL: 'all', - BPMN: '.bpmn', - PNG: '.png', - SVG: '.svg' + ALL: "all", + BPMN: ".bpmn", + PNG: ".png", + SVG: ".svg", }; diff --git a/components/bpmn-q/modeler-component/editor/ModelerHandler.js b/components/bpmn-q/modeler-component/editor/ModelerHandler.js index 57c55399..73ff3914 100644 --- a/components/bpmn-q/modeler-component/editor/ModelerHandler.js +++ b/components/bpmn-q/modeler-component/editor/ModelerHandler.js @@ -1,19 +1,22 @@ import BpmnModeler from "bpmn-js/lib/Modeler"; import BpmnPalletteModule from "bpmn-js/lib/features/palette"; import { - BpmnPropertiesPanelModule, - BpmnPropertiesProviderModule, - CamundaPlatformPropertiesProviderModule + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, + CamundaPlatformPropertiesProviderModule, } from "bpmn-js-properties-panel"; -import CamundaExtensionModule from 'camunda-bpmn-moddle/resources/camunda.json'; +import CamundaExtensionModule from "camunda-bpmn-moddle/resources/camunda.json"; import CustomPopupMenuModule from "./popup/"; -import { getAdditionalModules, getModdleExtension } from "./plugin/PluginHandler"; +import { + getAdditionalModules, + getModdleExtension, +} from "./plugin/PluginHandler"; import ModelerRulesModule from "./rules/"; -import LintModule from 'bpmn-js-bpmnlint'; -import bpmnlintConfig from '../../.bpmnlintrc'; +import LintModule from "bpmn-js-bpmnlint"; +import bpmnlintConfig from "../../.bpmnlintrc"; -import Clipboard from 'diagram-js/lib/features/clipboard/Clipboard'; -let camundaModdleDescriptor = require('camunda-bpmn-moddle/resources/camunda.json'); +import Clipboard from "diagram-js/lib/features/clipboard/Clipboard"; +let camundaModdleDescriptor = require("camunda-bpmn-moddle/resources/camunda.json"); /** * Handler which manages the creation of bpmn-js modeler instances. It controls the access to the modeler instance currently @@ -31,22 +34,21 @@ let modeler = undefined; * @returns {Modeler} The created bpmn-js modeler instance */ export function createModeler(containerId, propertiesParentId) { - - modeler = new BpmnModeler({ - container: containerId, - propertiesPanel: { - parent: propertiesParentId - }, - additionalModules: getModules(), - keyboard: { - bindTo: document - }, - linting: { - bpmnlint: bpmnlintConfig - }, - moddleExtensions: getExtensions(), - }); - return modeler; + modeler = new BpmnModeler({ + container: containerId, + propertiesPanel: { + parent: propertiesParentId, + }, + additionalModules: getModules(), + keyboard: { + bindTo: document, + }, + linting: { + bpmnlint: bpmnlintConfig, + }, + moddleExtensions: getExtensions(), + }); + return modeler; } /** @@ -55,17 +57,15 @@ export function createModeler(containerId, propertiesParentId) { * @return the created modeler */ export function createPlainModeler() { - return new BpmnModeler({ - additionalModules: [ - CamundaExtensionModule, - ], - keyboard: { - bindTo: document - }, - moddleExtensions: { - camunda: camundaModdleDescriptor, - }, - }); + return new BpmnModeler({ + additionalModules: [CamundaExtensionModule], + keyboard: { + bindTo: document, + }, + moddleExtensions: { + camunda: camundaModdleDescriptor, + }, + }); } /** @@ -75,102 +75,94 @@ export function createPlainModeler() { * @returns the created modeler */ export function createTempModeler() { - return new BpmnModeler({ - additionalModules: getModules(), - keyboard: { - bindTo: document - }, - moddleExtensions: getExtensions(), - }); + return new BpmnModeler({ + additionalModules: getModules(), + keyboard: { + bindTo: document, + }, + moddleExtensions: getExtensions(), + }); } /** - * Creates a modeler with all additional modules and extension moddles from all active plugins which is not - * saved in as the current modeler instance and load the given xml into it. - * - * @param xml the xml representing the BPMN diagram to load + * Create a Modeler with only Camunda native extensions and no additional modules * - * @returns the created modeler + * @returns the created bpmn-js modeler */ -export async function createTempModelerFromXml(xml) { - // create new modeler with the custom QuantME extensions - const bpmnModeler = createTempModeler(); - - // import the xml containing the definitions - try { - await bpmnModeler.importXML(xml); - return bpmnModeler; - } catch (err) { - console.error(err); - } - return undefined; +export function createLightweightModeler() { + return new BpmnModeler({ + moddleExtensions: getExtensions(), + }); } /** - * Creates a modeler with all additional modules and extension moddles from all active plugins which is - * saved as the current modeler instance and load the given xml into it. + * Creates a modeler with all additional modules and extension moddles from all active plugins which is not + * saved in as the current modeler instance and load the given xml into it. * * @param xml the xml representing the BPMN diagram to load * * @returns the created modeler */ -export async function createModelerFromXml(xml) { - // create new modeler with the custom QuantME extensions - const bpmnModeler = createModeler(); - - // import the xml containing the definitions - try { - await bpmnModeler.importXML(xml); - return bpmnModeler; - } catch (err) { - console.error(err); - } - return undefined; +export async function createTempModelerFromXml(xml) { + // create new modeler with the custom QuantME extensions + const bpmnModeler = createTempModeler(); + + // import the xml containing the definitions + try { + await bpmnModeler.importXML(xml); + return bpmnModeler; + } catch (err) { + console.error(err); + } + return undefined; } /** * Returns the current modeler instance rendered into the UI of the Quantum Workflow Modeler */ export function getModeler() { - return modeler; + return modeler; } /** * Returns all additional modules for the bpmn-js modeler necessary to use all modelling extensions of the active plugins. */ function getModules() { - const pluginModules = getAdditionalModules(); - var clipboardModule = { - 'clipboard': [ 'value', new Clipboard() ] - }; - let additionalModules = [ - BpmnPalletteModule, - BpmnPropertiesPanelModule, - BpmnPropertiesProviderModule, - CamundaPlatformPropertiesProviderModule, - CamundaExtensionModule, - CustomPopupMenuModule, - LintModule, - clipboardModule, - ModelerRulesModule - ].concat(pluginModules); - - console.log('\n Additional modules of the modeler: '); - console.log(additionalModules); - - return additionalModules; + const pluginModules = getAdditionalModules(); + var clipboardModule = { + clipboard: ["value", new Clipboard()], + }; + let additionalModules = [ + BpmnPalletteModule, + BpmnPropertiesPanelModule, + BpmnPropertiesProviderModule, + CamundaPlatformPropertiesProviderModule, + CamundaExtensionModule, + CustomPopupMenuModule, + LintModule, + clipboardModule, + ModelerRulesModule, + ].concat(pluginModules); + + console.log("\n Additional modules of the modeler: "); + console.log(additionalModules); + + return additionalModules; } /** * Returns all moddle extensions for the bpmn-js modeler necessary to use all modelling extensions of the active plugins. */ function getExtensions() { - let moddleExtension = Object.assign({ - camunda: camundaModdleDescriptor, - }, getModdleExtension()); + let moddleExtension = Object.assign( + { + camunda: camundaModdleDescriptor, + }, + getModdleExtension() + ); - console.log('\n Moddle extensions of the modeler: '); - console.log(moddleExtension); + console.log("\n Moddle extensions of the modeler: "); + console.log(moddleExtension); - return moddleExtension; + return moddleExtension; } diff --git a/components/bpmn-q/modeler-component/editor/config/ConfigModal.js b/components/bpmn-q/modeler-component/editor/config/ConfigModal.js index 4de98bed..9ad4d758 100644 --- a/components/bpmn-q/modeler-component/editor/config/ConfigModal.js +++ b/components/bpmn-q/modeler-component/editor/config/ConfigModal.js @@ -10,14 +10,14 @@ */ /* eslint-disable no-unused-vars */ -import React from 'react'; -import Modal from '../ui/modal/Modal'; -import './config-modal.css'; +import React from "react"; +import Modal from "../ui/modal/Modal"; +import "./config-modal.css"; // polyfill upcoming structural components -const Title = Modal.Title || (({children}) =>

{children}

); -const Body = Modal.Body || (({children}) =>
{children}
); -const Footer = Modal.Footer || (({children}) =>
{children}
); +const Title = Modal.Title || (({ children }) =>

{children}

); +const Body = Modal.Body || (({ children }) =>
{children}
); +const Footer = Modal.Footer || (({ children }) =>
{children}
); /** * Configuration modal of the editor which displays a set of given configTabs. used to display customized tabs of the @@ -28,64 +28,88 @@ const Footer = Modal.Footer || (({children}) =>
{children}
); * @returns {JSX.Element} The modal as React component * @constructor */ -export default function ConfigModal({onClose, configTabs}) { +export default function ConfigModal({ onClose, configTabs }) { + // return the new values to the config plugin + const onSubmit = () => { + // call close callback + onClose(); - // return the new values to the config plugin - const onSubmit = () => { - - // call close callback - onClose(); - - for (let tab of configTabs) { - - // call close callback for each tab to allow custom cleanups - tab.configTab.prototype.onClose(); - } - }; + for (let tab of configTabs) { + // call close callback for each tab to allow custom cleanups + tab.configTab.prototype.onClose(); + } + }; - // refs to enable changing the state through the plugin - let elementsRootRef = React.createRef(); + // refs to enable changing the state through the plugin + let elementsRootRef = React.createRef(); - // method to enable button functionality by hiding and displaying different div elements - function openTab(tabName, id) { - console.log(id); - const elements = elementsRootRef.current.children; + // method to enable button functionality by hiding and displaying different div elements + function openTab(tabName, id) { + console.log(id); + const elements = elementsRootRef.current.children; - for (let i = 0; i < elements.length; i++) { - elements[i].hidden = true; - } - elements[id].hidden = false; + for (let i = 0; i < elements.length; i++) { + elements[i].hidden = true; } + elements[id].hidden = false; + } - return - - Modeler Configuration - + return ( + + Modeler Configuration - -
-
-
- {React.Children.toArray(configTabs.map((tab, index) => ))} -
- -
- {React.Children.toArray(configTabs.map((tab, index) => ))} -
-
-
- + +
+
+
+ {React.Children.toArray( + configTabs.map((tab, index) => ( + + )) + )} +
-
-
- - +
+ {React.Children.toArray( + configTabs.map((tab, index) => ( + + )) + )}
-
- ; -} +
+
+ +
+
+ + +
+
+
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/config/ConfigPlugin.js b/components/bpmn-q/modeler-component/editor/config/ConfigPlugin.js index bde3f03a..d0be2867 100644 --- a/components/bpmn-q/modeler-component/editor/config/ConfigPlugin.js +++ b/components/bpmn-q/modeler-component/editor/config/ConfigPlugin.js @@ -9,60 +9,65 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, {PureComponent, Fragment} from 'react'; +import React, { PureComponent, Fragment } from "react"; -import ConfigModal from './ConfigModal'; -import {getModeler} from "../ModelerHandler"; -import {getConfigTabs} from "../plugin/PluginHandler"; +import ConfigModal from "./ConfigModal"; +import { getModeler } from "../ModelerHandler"; +import { getConfigTabs } from "../plugin/PluginHandler"; export default class ConfigPlugin extends PureComponent { + constructor(props) { + super(props); - constructor(props) { - super(props); + this.state = { + configOpen: false, + }; - this.state = { - configOpen: false, - }; + this.handleConfigClosed = this.handleConfigClosed.bind(this); + } - this.handleConfigClosed = this.handleConfigClosed.bind(this); - } - - componentDidMount() { - - // get current modeler instance - this.modeler = getModeler(); + componentDidMount() { + // get current modeler instance + this.modeler = getModeler(); - // set up config of the modeler - if (!this.modeler.config) { - this.modeler.config = {}; + // set up config of the modeler + if (!this.modeler.config) { + this.modeler.config = {}; - for (let tab of getConfigTabs()) { - tab.configTab.prototype.config(); - } - } + for (let tab of getConfigTabs()) { + tab.configTab.prototype.config(); + } } + } - // callback function to close the config modal - handleConfigClosed() { - this.setState({configOpen: false}); - } - - render() { + // callback function to close the config modal + handleConfigClosed() { + this.setState({ configOpen: false }); + } - // render config button and pop-up menu - return ( -
- -
- {this.state.configOpen && ( - - )} -
); - } + render() { + // render config button and pop-up menu + return ( + +
+ +
+ {this.state.configOpen && ( + + )} +
+ ); + } } diff --git a/components/bpmn-q/modeler-component/editor/config/EditorConfigManager.js b/components/bpmn-q/modeler-component/editor/config/EditorConfigManager.js index 43b7baf2..b97be0da 100644 --- a/components/bpmn-q/modeler-component/editor/config/EditorConfigManager.js +++ b/components/bpmn-q/modeler-component/editor/config/EditorConfigManager.js @@ -1,14 +1,18 @@ -import { getPluginConfig } from '../plugin/PluginConfigHandler'; -import { saveFileFormats, transformedWorkflowHandlers, autoSaveFile } from '../EditorConstants'; +import { getPluginConfig } from "../plugin/PluginConfigHandler"; +import { + saveFileFormats, + transformedWorkflowHandlers, + autoSaveFile, +} from "../EditorConstants"; // default configurations of the editor const defaultConfig = { - camundaEndpoint: process.env.CAMUNDA_ENDPOINT, - fileName: process.env.DOWNLOAD_FILE_NAME, - transformedWorkflowHandler: transformedWorkflowHandlers.NEW_TAB, - autoSaveFileOption: autoSaveFile.INTERVAL, - fileFormat: saveFileFormats.BPMN, - autoSaveIntervalSize: process.env.AUTOSAVE_INTERVAL + camundaEndpoint: process.env.CAMUNDA_ENDPOINT, + fileName: process.env.DOWNLOAD_FILE_NAME, + transformedWorkflowHandler: transformedWorkflowHandlers.NEW_TAB, + autoSaveFileOption: autoSaveFile.INTERVAL, + fileFormat: saveFileFormats.BPMN, + autoSaveIntervalSize: process.env.AUTOSAVE_INTERVAL, }; let config = {}; @@ -19,10 +23,12 @@ let config = {}; * @return {string} the currently specified endpoint of the Camunda engine */ export function getCamundaEndpoint() { - if (config.camundaEndpoint === undefined) { - setCamundaEndpoint(getPluginConfig('editor').camundaEndpoint || defaultConfig.camundaEndpoint); - } - return config.camundaEndpoint; + if (config.camundaEndpoint === undefined) { + setCamundaEndpoint( + getPluginConfig("editor").camundaEndpoint || defaultConfig.camundaEndpoint + ); + } + return config.camundaEndpoint; } /** @@ -31,11 +37,10 @@ export function getCamundaEndpoint() { * @param camundaEndpoint the endpoint of the Camunda engine */ export function setCamundaEndpoint(camundaEndpoint) { - if (camundaEndpoint !== null && camundaEndpoint !== undefined) { - - // remove trailing slashes - config.camundaEndpoint = camundaEndpoint.replace(/\/$/, ''); - } + if (camundaEndpoint !== null && camundaEndpoint !== undefined) { + // remove trailing slashes + config.camundaEndpoint = camundaEndpoint.replace(/\/$/, ""); + } } /** @@ -44,10 +49,10 @@ export function setCamundaEndpoint(camundaEndpoint) { * @return {string} the file name */ export function getFileName() { - if (config.fileName === undefined) { - setFileName(getPluginConfig('editor').fileName || defaultConfig.fileName); - } - return config.fileName; + if (config.fileName === undefined) { + setFileName(getPluginConfig("editor").fileName || defaultConfig.fileName); + } + return config.fileName; } /** @@ -56,11 +61,10 @@ export function getFileName() { * @param fileName the new file name */ export function setFileName(fileName) { - if (fileName !== null && fileName !== undefined) { - - // remove trailing slashes - config.fileName = fileName; - } + if (fileName !== null && fileName !== undefined) { + // remove trailing slashes + config.fileName = fileName; + } } /** @@ -69,11 +73,16 @@ export function setFileName(fileName) { * @return {string} the currently specified handler id */ export function getTransformedWorkflowHandler() { - if (config.transformedWorkflowHandler === undefined) { - const workflowHandler = transformedWorkflowHandlers[getPluginConfig('editor').transformedWorkflowHandler]; - setTransformedWorkflowHandler(workflowHandler || defaultConfig.transformedWorkflowHandler); - } - return config.transformedWorkflowHandler; + if (config.transformedWorkflowHandler === undefined) { + const workflowHandler = + transformedWorkflowHandlers[ + getPluginConfig("editor").transformedWorkflowHandler + ]; + setTransformedWorkflowHandler( + workflowHandler || defaultConfig.transformedWorkflowHandler + ); + } + return config.transformedWorkflowHandler; } /** @@ -82,13 +91,17 @@ export function getTransformedWorkflowHandler() { * @param transformedWorkflowHandler the id of the transformed workflow handler */ export function setTransformedWorkflowHandler(transformedWorkflowHandler) { - if (transformedWorkflowHandler !== null && transformedWorkflowHandler !== undefined - // check that the new value is a valid handler id - && Object.values(transformedWorkflowHandlers).includes(transformedWorkflowHandler)) { - - // remove trailing slashes - config.transformedWorkflowHandler = transformedWorkflowHandler; - } + if ( + transformedWorkflowHandler !== null && + transformedWorkflowHandler !== undefined && + // check that the new value is a valid handler id + Object.values(transformedWorkflowHandlers).includes( + transformedWorkflowHandler + ) + ) { + // remove trailing slashes + config.transformedWorkflowHandler = transformedWorkflowHandler; + } } /** @@ -97,11 +110,14 @@ export function setTransformedWorkflowHandler(transformedWorkflowHandler) { * @return {string} the currently specified handler id */ export function getAutoSaveFileOption() { - if (config.autoSaveFileOption === undefined) { - const autoSaveFileOption = autoSaveFile[getPluginConfig('editor').autoSaveFileOption]; - setAutoSaveFileOption(autoSaveFileOption || defaultConfig.autoSaveFileOption); - } - return config.autoSaveFileOption; + if (config.autoSaveFileOption === undefined) { + const autoSaveFileOption = + autoSaveFile[getPluginConfig("editor").autoSaveFileOption]; + setAutoSaveFileOption( + autoSaveFileOption || defaultConfig.autoSaveFileOption + ); + } + return config.autoSaveFileOption; } /** @@ -110,25 +126,27 @@ export function getAutoSaveFileOption() { * @param autoSaveFileOption the id of the transformed workflow handler */ export function setAutoSaveFileOption(autoSaveFileOption) { - if (autoSaveFileOption !== null && autoSaveFileOption !== undefined - // check that the new value is a valid handler id - && Object.values(autoSaveFile).includes(autoSaveFileOption)) { - - config.autoSaveFileOption = autoSaveFileOption; - } + if ( + autoSaveFileOption !== null && + autoSaveFileOption !== undefined && + // check that the new value is a valid handler id + Object.values(autoSaveFile).includes(autoSaveFileOption) + ) { + config.autoSaveFileOption = autoSaveFileOption; + } } -/** +/** * Get the file format * * @return {string} the currently specified handler id */ export function getFileFormat() { - if (config.fileFormat === undefined) { - const fileFormat = saveFileFormats[getPluginConfig('editor').fileFormat]; - setFileFormat(fileFormat || defaultConfig.fileFormat); - } - return config.fileFormat; + if (config.fileFormat === undefined) { + const fileFormat = saveFileFormats[getPluginConfig("editor").fileFormat]; + setFileFormat(fileFormat || defaultConfig.fileFormat); + } + return config.fileFormat; } /** @@ -137,40 +155,45 @@ export function getFileFormat() { * @param fileFormat the file format */ export function setFileFormat(fileFormat) { - if (fileFormat !== null && fileFormat !== undefined - // check that the new value is a valid handler id - && Object.values(saveFileFormats).includes(fileFormat)) { - - config.fileFormat = fileFormat; - } + if ( + fileFormat !== null && + fileFormat !== undefined && + // check that the new value is a valid handler id + Object.values(saveFileFormats).includes(fileFormat) + ) { + config.fileFormat = fileFormat; + } } -/** +/** * Get the autosave interval size * * @return {string} the current interval size */ export function getAutoSaveIntervalSize() { - if (config.autoSaveIntervalSize === undefined) { - setAutoSaveIntervalSize(getPluginConfig('editor').autoSaveIntervalSize || defaultConfig.autoSaveIntervalSize); - } - return config.autoSaveIntervalSize; + if (config.autoSaveIntervalSize === undefined) { + setAutoSaveIntervalSize( + getPluginConfig("editor").autoSaveIntervalSize || + defaultConfig.autoSaveIntervalSize + ); + } + return config.autoSaveIntervalSize; } /** * Set the interval size of the autosave function * - * @param intervalSize the interval size + * @param intervalSize the interval size */ export function setAutoSaveIntervalSize(intervalSize) { - if (intervalSize !== null && intervalSize !== undefined) { - config.autoSaveIntervalSize = intervalSize; - } + if (intervalSize !== null && intervalSize !== undefined) { + config.autoSaveIntervalSize = intervalSize; + } } /** * Resets the current editor configs */ export function reset() { - config = {}; -} \ No newline at end of file + config = {}; +} diff --git a/components/bpmn-q/modeler-component/editor/config/GeneralTab.js b/components/bpmn-q/modeler-component/editor/config/GeneralTab.js index 4f6f822a..ce447f48 100644 --- a/components/bpmn-q/modeler-component/editor/config/GeneralTab.js +++ b/components/bpmn-q/modeler-component/editor/config/GeneralTab.js @@ -1,7 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; import { getModeler } from "../ModelerHandler"; import * as editorConfig from "./EditorConfigManager"; -import { autoSaveFile, saveFileFormats, transformedWorkflowHandlers } from '../EditorConstants'; +import { + autoSaveFile, + saveFileFormats, + transformedWorkflowHandlers, +} from "../EditorConstants"; /** * Tab for the ConfigModal. Used to allow the configurations of the editor configs, namely the camunda endpoint and the @@ -11,157 +15,173 @@ import { autoSaveFile, saveFileFormats, transformedWorkflowHandlers } from '../E * @constructor */ export default function EditorTab() { + const [camundaEndpoint, setCamundaEndpoint] = useState( + editorConfig.getCamundaEndpoint() + ); + const [workflowHandler, setWorkflowHandler] = useState( + editorConfig.getTransformedWorkflowHandler() + ); + const [autoSaveFileOption, setAutoSaveFileOption] = useState( + editorConfig.getAutoSaveFileOption() + ); + const [fileName, setFileName] = useState(editorConfig.getFileName()); + const [fileFormat, setFileFormat] = useState(editorConfig.getFileFormat()); + const [autoSaveIntervalSize, setAutoSaveIntervalSize] = useState( + editorConfig.getAutoSaveIntervalSize() + ); - const [camundaEndpoint, setCamundaEndpoint] = useState(editorConfig.getCamundaEndpoint()); - const [workflowHandler, setWorkflowHandler] = useState(editorConfig.getTransformedWorkflowHandler()); - const [autoSaveFileOption, setAutoSaveFileOption] = useState(editorConfig.getAutoSaveFileOption()); - const [fileName, setFileName] = useState(editorConfig.getFileName()); - const [fileFormat, setFileFormat] = useState(editorConfig.getFileFormat()); - const [autoSaveIntervalSize, setAutoSaveIntervalSize] = useState(editorConfig.getAutoSaveIntervalSize()); + const modeler = getModeler(); + const editorActions = modeler.get("editorActions"); - const modeler = getModeler(); - - const editorActions = modeler.get('editorActions'); - - // register listener for editor action to get changes on the camunda endpoint - if (!editorActions._actions.hasOwnProperty('camundaEndpointChanged')) { - editorActions.register({ - camundaEndpointChanged: function (camundaEndpoint) { - modeler.config.camundaEndpoint = camundaEndpoint; - } - }); - } - - // register listener for editor action to get changes on the camunda endpoint - if (!editorActions._actions.hasOwnProperty('fileNameChanged')) { - editorActions.register({ - fileNameChanged: function (fileName) { - modeler.config.fileName = fileName; - } - }); - } - - // save values of the tab entries in the editor config - EditorTab.prototype.onClose = () => { + // register listener for editor action to get changes on the camunda endpoint + if (!editorActions._actions.hasOwnProperty("camundaEndpointChanged")) { + editorActions.register({ + camundaEndpointChanged: function (camundaEndpoint) { modeler.config.camundaEndpoint = camundaEndpoint; - modeler.config.fileName = fileName; - editorConfig.setCamundaEndpoint(camundaEndpoint); - editorConfig.setTransformedWorkflowHandler(workflowHandler); - editorConfig.setAutoSaveFileOption(autoSaveFileOption); - modeler.get('eventBus').fire('autoSaveOptionChanged', { autoSaveFileOption }); - editorConfig.setFileName(fileName); - editorConfig.setFileFormat(fileFormat); - editorConfig.setAutoSaveIntervalSize(autoSaveIntervalSize); - }; + }, + }); + } - // return tab which contains entries to change the camunda endpoint and the workflow handler - return (<> -

Workflow Engine configuration:

- - - - - - - -
Camunda Engine Endpoint - setCamundaEndpoint(event.target.value)} /> -
-

Handle for transformed workflows:

- - - - - - - -
Transformed Workflow Handler - + // register listener for editor action to get changes on the camunda endpoint + if (!editorActions._actions.hasOwnProperty("fileNameChanged")) { + editorActions.register({ + fileNameChanged: function (fileName) { + modeler.config.fileName = fileName; + }, + }); + } -
-

Download workflow:

- - - - - - - - - - - -
Download file name - setFileName(event.target.value)} /> -
Download file format - + // save values of the tab entries in the editor config + EditorTab.prototype.onClose = () => { + modeler.config.camundaEndpoint = camundaEndpoint; + modeler.config.fileName = fileName; + editorConfig.setCamundaEndpoint(camundaEndpoint); + editorConfig.setTransformedWorkflowHandler(workflowHandler); + editorConfig.setAutoSaveFileOption(autoSaveFileOption); + modeler + .get("eventBus") + .fire("autoSaveOptionChanged", { autoSaveFileOption }); + editorConfig.setFileName(fileName); + editorConfig.setFileFormat(fileFormat); + editorConfig.setAutoSaveIntervalSize(autoSaveIntervalSize); + }; -
-

Auto save file:

- - - - - - - {autoSaveFileOption === autoSaveFile.INTERVAL && ( - - - - - )} - -
Auto save file option: - -
Auto save interval size: - setAutoSaveIntervalSize(event.target.value)} /> -
- ); + // return tab which contains entries to change the camunda endpoint and the workflow handler + return ( + <> +

Workflow Engine configuration:

+ + + + + + + +
Camunda Engine Endpoint + setCamundaEndpoint(event.target.value)} + /> +
+

Handle for transformed workflows:

+ + + + + + + +
Transformed Workflow Handler + +
+

Download workflow:

+ + + + + + + + + + + +
Download file name + setFileName(event.target.value)} + /> +
Download file format + +
+

Auto save file:

+ + + + + + + {autoSaveFileOption === autoSaveFile.INTERVAL && ( + + + + + )} + +
Auto save file option: + +
Auto save interval size: + + setAutoSaveIntervalSize(event.target.value) + } + /> +
+ + ); } EditorTab.prototype.config = () => { - const modeler = getModeler(); + const modeler = getModeler(); - modeler.config.camundaEndpoint = editorConfig.getCamundaEndpoint(); - modeler.config.fileName = editorConfig.getFileName(); -}; \ No newline at end of file + modeler.config.camundaEndpoint = editorConfig.getCamundaEndpoint(); + modeler.config.fileName = editorConfig.getFileName(); +}; diff --git a/components/bpmn-q/modeler-component/editor/config/config-modal.css b/components/bpmn-q/modeler-component/editor/config/config-modal.css index cada1cf4..da914a5b 100644 --- a/components/bpmn-q/modeler-component/editor/config/config-modal.css +++ b/components/bpmn-q/modeler-component/editor/config/config-modal.css @@ -1,53 +1,53 @@ .qwm-innerConfig { - min-width: 100px; - max-width: 100px; - min-height: auto; - height: auto; - border-radius: 3px; - font-size: 13px; - font-weight: bold; - font-stretch: normal; - font-style: normal; - line-height: normal; - letter-spacing: normal; - text-align: center; - padding: 5px; + min-width: 100px; + max-width: 100px; + min-height: auto; + height: auto; + border-radius: 3px; + font-size: 13px; + font-weight: bold; + font-stretch: normal; + font-style: normal; + line-height: normal; + letter-spacing: normal; + text-align: center; + padding: 5px; } .qwm-innerConfig + .qwm-innerConfig { - margin-top: 5px; + margin-top: 5px; } .qwm-indent { - margin-left: 5px; + margin-left: 5px; } .qwm-btn-config:before { - content: ""; - display: block; - width: 15px; - height: 15px; - background-size: contain; - background: url("../resources/icons/config-icon.png") no-repeat center center; - float: left; + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../resources/icons/config-icon.png") no-repeat center center; + float: left; } .qwm-btn-primary:focus { - border: solid 1px var(--blue-darken-48); - background-color: var(--blue-darken-55); + border: solid 1px var(--blue-darken-48); + background-color: var(--blue-darken-55); } .qwm-spaceAbove { - padding-top: 1em; - margin-left: 10px; + padding-top: 1em; + margin-left: 10px; } .qwm-tabButtonsContainer { - display: flex; - flex-direction: column; - align-items: flex-start; - overflow: auto; - min-width: 120px; - max-height: 263px; - direction: rtl; + display: flex; + flex-direction: column; + align-items: flex-start; + overflow: auto; + min-width: 120px; + max-height: 263px; + direction: rtl; } diff --git a/components/bpmn-q/modeler-component/editor/configurations/ConfigurationEndpoint.js b/components/bpmn-q/modeler-component/editor/configurations/ConfigurationEndpoint.js index 7cfc8c4c..8833c8ce 100644 --- a/components/bpmn-q/modeler-component/editor/configurations/ConfigurationEndpoint.js +++ b/components/bpmn-q/modeler-component/editor/configurations/ConfigurationEndpoint.js @@ -2,53 +2,61 @@ * Class to fetch and store Configurations from an external repository. The used repository can be configured in the constructor. */ export default class ConfigurationsEndpoint { + // array containing the fetched configurations + _configurations = []; - // array containing the fetched configurations - _configurations = []; + constructor(endpointUrl) { + this._endpointUrl = endpointUrl; - constructor(endpointUrl) { - this._endpointUrl = endpointUrl; + // initial fetch for configurations + this.fetchConfigurations(); + } - // initial fetch for configurations - this.fetchConfigurations(); - } + /** + * Fetch the configured endpoint and store the result in this._configurations + */ + fetchConfigurations() { + fetch(this._endpointUrl) + .then((response) => + response.headers.get("content-type") === "text/plain; charset=utf-8" + ? response.text() + : response.json() + ) + .then((data) => { + this._configurations = + typeof data === "string" ? JSON.parse(data) : data; + console.log(this._configurations); + }) + .catch((error) => { + console.error( + "Error fetching configurations from " + + this._endpointUrl + + ": \n" + + error + ); + }); + } - /** - * Fetch the configured endpoint and store the result in this._configurations - */ - fetchConfigurations() { - fetch(this._endpointUrl) - .then(response => response.headers.get('content-type') === 'text/plain; charset=utf-8' ? response.text() : response.json()) - .then(data => { - this._configurations = typeof data === "string" ? JSON.parse(data) : data; - console.log(this._configurations); - }) - .catch(error => { - console.error('Error fetching configurations from ' + this._endpointUrl + ': \n' + error); - }); - } + /** + * Returns all stored configurations which apply to the given type. + * + * @param type The type the wanted configurations are applied to. + * @returns {*[]} All configurations of this._configurations which apply to the given type. + */ + getConfigurations(type) { + // return all configurations which apply to the given type + return this._configurations.filter(function (configuration) { + return configuration.appliesTo === type; + }); + } - /** - * Returns all stored configurations which apply to the given type. - * - * @param type The type the wanted configurations are applied to. - * @returns {*[]} All configurations of this._configurations which apply to the given type. - */ - getConfigurations(type) { - - // return all configurations which apply to the given type - return this._configurations.filter(function (configuration) { - return configuration.appliesTo === type; - }); - } - - /** - * Returns the configurations which has the given id. - * - * @param id The id of the searched configuration. - * @returns {*} The configuration with the given id. - */ - getConfiguration(id) { - return this._configurations.find(config => config.id === id); - } -} \ No newline at end of file + /** + * Returns the configurations which has the given id. + * + * @param id The id of the searched configuration. + * @returns {*} The configuration with the given id. + */ + getConfiguration(id) { + return this._configurations.find((config) => config.id === id); + } +} diff --git a/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsProperties.js b/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsProperties.js index af2ad42c..9a1fc2e4 100644 --- a/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsProperties.js +++ b/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsProperties.js @@ -1,11 +1,19 @@ -import {isTextFieldEntryEdited, TextFieldEntry, CheckboxEntry, isCheckboxEntryEdited} from "@bpmn-io/properties-panel"; -import {useService} from 'bpmn-js-properties-panel'; -import {nextId} from '../util/camunda-utils/ElementUtil'; import { - addAttributeValueToCamundaIO, addAttributeValueToKeyValueMap, getAttributeValue, - getAttributeValueFromCamundaIO, - getAttributeValueFromKeyValueMap, setAttributeValue -} from './ConfigurationsUtil'; + isTextFieldEntryEdited, + TextFieldEntry, + CheckboxEntry, + isCheckboxEntryEdited, +} from "@bpmn-io/properties-panel"; +import { useService } from "bpmn-js-properties-panel"; +import { nextId } from "../util/camunda-utils/ElementUtil"; +import { + addAttributeValueToCamundaIO, + addAttributeValueToKeyValueMap, + getAttributeValue, + getAttributeValueFromCamundaIO, + getAttributeValueFromKeyValueMap, + setAttributeValue, +} from "./ConfigurationsUtil"; /** * Creates entries for the properties panel based on the attributes of the given configuration. @@ -16,62 +24,80 @@ import { * @param configuration The configuration for which the properties should be generated. * @returns {*} The created properties panel entries. */ -export default function ConfigurationsProperties(element, injector, translate, configuration) { - - const bpmnFactory = injector.get('bpmnFactory'); - const modeling = injector.get('modeling'); - const commandStack = injector.get('commandStack'); - - // generate entries based on the attributes of the configuration and their definitions - return configuration.attributes.map(function (attribute) { - - // do not display hidden attributes - if (attribute.hide) { - return {}; - } - - let component; - let isEdited; - switch (attribute.type) { - case 'Boolean': - component = BooleanEntry; - isEdited = isCheckboxEntryEdited; - break; - default: // String - component = TextEntry; - isEdited = isTextFieldEntryEdited; - break; - } - - // set setter and getter depending on the type of the bindTo attribute - let setValue; - let getValue; - switch (attribute.bindTo.type) { - case 'camunda:InputParameter': - case 'camunda:OutputParameter': - setValue = addAttributeValueToCamundaIO(element, bpmnFactory, attribute.bindTo.type, attribute, modeling); - getValue = getAttributeValueFromCamundaIO(element, bpmnFactory, attribute.bindTo.type); - break; - case 'KeyValueMap': - setValue = addAttributeValueToKeyValueMap(element, attribute, bpmnFactory, commandStack); - getValue = getAttributeValueFromKeyValueMap(element); - break; - default: - setValue = setAttributeValue(element, attribute, modeling); - getValue = getAttributeValue(element); - break; - } - - return { - id: nextId(attribute.name), - attribute, - setValue: setValue, - getValue: getValue, - component: component, - isEdited: isEdited, - disabled: true, - }; - }); +export default function ConfigurationsProperties( + element, + injector, + translate, + configuration +) { + const bpmnFactory = injector.get("bpmnFactory"); + const modeling = injector.get("modeling"); + const commandStack = injector.get("commandStack"); + + // generate entries based on the attributes of the configuration and their definitions + return configuration.attributes.map(function (attribute) { + // do not display hidden attributes + if (attribute.hide) { + return {}; + } + + let component; + let isEdited; + switch (attribute.type) { + case "Boolean": + component = BooleanEntry; + isEdited = isCheckboxEntryEdited; + break; + default: // String + component = TextEntry; + isEdited = isTextFieldEntryEdited; + break; + } + + // set setter and getter depending on the type of the bindTo attribute + let setValue; + let getValue; + switch (attribute.bindTo.type) { + case "camunda:InputParameter": + case "camunda:OutputParameter": + setValue = addAttributeValueToCamundaIO( + element, + bpmnFactory, + attribute.bindTo.type, + attribute, + modeling + ); + getValue = getAttributeValueFromCamundaIO( + element, + bpmnFactory, + attribute.bindTo.type + ); + break; + case "KeyValueMap": + setValue = addAttributeValueToKeyValueMap( + element, + attribute, + bpmnFactory, + commandStack + ); + getValue = getAttributeValueFromKeyValueMap(element); + break; + default: + setValue = setAttributeValue(element, attribute, modeling); + getValue = getAttributeValue(element); + break; + } + + return { + id: nextId(attribute.name), + attribute, + setValue: setValue, + getValue: getValue, + component: component, + isEdited: isEdited, + disabled: true, + }; + }); } /** @@ -81,25 +107,20 @@ export default function ConfigurationsProperties(element, injector, translate, c * @returns {preact.VNode} */ function TextEntry(props) { - const { - idPrefix, - attribute, - setValue, - getValue, - } = props; - - const translate = useService('translate'); - const debounce = useService('debounceInput'); - - return TextFieldEntry({ - element: attribute, - id: idPrefix + '-value', - label: translate(attribute.label), - disabled: attribute.disable, - getValue, - setValue, - debounce - }); + const { idPrefix, attribute, setValue, getValue } = props; + + const translate = useService("translate"); + const debounce = useService("debounceInput"); + + return TextFieldEntry({ + element: attribute, + id: idPrefix + "-value", + label: translate(attribute.label), + disabled: attribute.disable, + getValue, + setValue, + debounce, + }); } /** @@ -109,33 +130,27 @@ function TextEntry(props) { * @returns {preact.VNode} */ function BooleanEntry(props) { - const { - idPrefix, - attribute, - setValue, - getValue, - } = props; - - console.log(attribute.label); - - const translate = useService('translate'); - - const getBoolValue = (attribute) => { - const boolStr = getValue(attribute); - try { - return JSON.parse(boolStr); - } catch (error) { - console.log(`Failed to parse ${boolStr} to boolean.`); - } - - }; - - return CheckboxEntry({ - element: attribute, - id: idPrefix + '-value', - label: translate(attribute.label), - disabled: attribute.disable, - getValue: getBoolValue, - setValue, - }); -} \ No newline at end of file + const { idPrefix, attribute, setValue, getValue } = props; + + console.log(attribute.label); + + const translate = useService("translate"); + + const getBoolValue = (attribute) => { + const boolStr = getValue(attribute); + try { + return JSON.parse(boolStr); + } catch (error) { + console.log(`Failed to parse ${boolStr} to boolean.`); + } + }; + + return CheckboxEntry({ + element: attribute, + id: idPrefix + "-value", + label: translate(attribute.label), + disabled: attribute.disable, + getValue: getBoolValue, + setValue, + }); +} diff --git a/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsUtil.js b/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsUtil.js index 7b2c0043..72124964 100644 --- a/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsUtil.js +++ b/components/bpmn-q/modeler-component/editor/configurations/ConfigurationsUtil.js @@ -1,12 +1,12 @@ -import * as configConsts from './Constants'; -import {getBusinessObject} from 'bpmn-js/lib/util/ModelUtil'; -import * as dataConsts from '../../extensions/data-extension/Constants'; +import * as configConsts from "./Constants"; +import { getBusinessObject } from "bpmn-js/lib/util/ModelUtil"; +import * as dataConsts from "../../extensions/data-extension/Constants"; import { - addCamundaInputMapParameter, - addCamundaOutputMapParameter, - getCamundaInputOutput -} from '../util/ModellingUtilities'; -import * as configsConsts from './Constants'; + addCamundaInputMapParameter, + addCamundaOutputMapParameter, + getCamundaInputOutput, +} from "../util/ModellingUtilities"; +import * as configsConsts from "./Constants"; /** * Create popup menu entries for a given array of configurations. Per default each entry applies its configuration to the @@ -22,43 +22,55 @@ import * as configsConsts from './Constants'; * @param action Optional action which will be triggered when an entry is selected. * @returns {{}} The list of popup menu entries. */ -export function createConfigurationsEntries(element, className, configurations, bpmnFactory, modeling, commandStack, replaceElement, action = undefined) { - - const menuEntries = {}; - let updateAction; - - console.log('Create entries for configurations:'); - console.log(configurations); - - configurations.map(function (config) { - - // define action for the entry - if (action) { - updateAction = function (event) { - action(event, config); - }; - } else { - updateAction = function () { - - // replace element with configuration type if types mismatch - let newElement; - if (element.type !== config.appliesTo) { - newElement = replaceElement(element, {type: config.appliesTo}); - } - - handleConfigurationsAction(newElement || element, config, bpmnFactory, modeling, commandStack); - }; +export function createConfigurationsEntries( + element, + className, + configurations, + bpmnFactory, + modeling, + commandStack, + replaceElement, + action = undefined +) { + const menuEntries = {}; + let updateAction; + + console.log("Create entries for configurations:"); + console.log(configurations); + + configurations.map(function (config) { + // define action for the entry + if (action) { + updateAction = function (event) { + action(event, config); + }; + } else { + updateAction = function () { + // replace element with configuration type if types mismatch + let newElement; + if (element.type !== config.appliesTo) { + newElement = replaceElement(element, { type: config.appliesTo }); } - // create popup menu entry - menuEntries[config.id] = { - label: config.name, - className: className, - action: updateAction, - }; - }); + handleConfigurationsAction( + newElement || element, + config, + bpmnFactory, + modeling, + commandStack + ); + }; + } - return menuEntries; + // create popup menu entry + menuEntries[config.id] = { + label: config.name, + className: className, + action: updateAction, + }; + }); + + return menuEntries; } /** @@ -70,47 +82,72 @@ export function createConfigurationsEntries(element, className, configurations, * @param modeling modeling dependency of the modeler instance. * @param commandStack dependency of the modeler instance. */ -export function handleConfigurationsAction(element, config, bpmnFactory, modeling, commandStack) { - - // save id of selected configuration in the element +export function handleConfigurationsAction( + element, + config, + bpmnFactory, + modeling, + commandStack +) { + // save id of selected configuration in the element + modeling.updateProperties(element, { + [configConsts.SELECT_CONFIGURATIONS_ID]: config.id, + }); + + // save icon property if defined of the selected configuration in the element to allow customized rendering + if (config.icon) { modeling.updateProperties(element, { - [configConsts.SELECT_CONFIGURATIONS_ID]: config.id, + [configsConsts.CONFIGURATIONS_ICON]: JSON.stringify(config.icon), }); - - // save icon property if defined of the selected configuration in the element to allow customized rendering - if (config.icon) { - modeling.updateProperties(element, { - [configsConsts.CONFIGURATIONS_ICON]: JSON.stringify(config.icon), - }); + } + + // set name of the element to configuration name + modeling.updateProperties(element, { + name: config.name, + }); + + config.attributes.forEach(function (attribute) { + // set properties based on the type of the bindTo value + switch (attribute.bindTo.type) { + case "camunda:InputParameter": + case "camunda:OutputParameter": + addAttributeValueToCamundaIO( + element, + bpmnFactory, + attribute.bindTo.type, + attribute, + modeling + )(attribute.value); + break; + case "camunda:InputMapParameter": + addCamundaInputMapParameter( + element.businessObject, + attribute.name, + attribute.value, + bpmnFactory + ); + break; + case "camunda:OutputMapParameter": + addCamundaOutputMapParameter( + element.businessObject, + attribute.name, + attribute.value, + bpmnFactory + ); + break; + case "KeyValueMap": + addAttributeValueToKeyValueMap( + element, + attribute, + bpmnFactory, + commandStack + )(attribute.value); + break; + default: + setAttributeValue(element, attribute, modeling)(attribute.value); + break; } - - // set name of the element to configuration name - modeling.updateProperties(element, { - name: config.name, - }); - - config.attributes.forEach(function (attribute) { - - // set properties based on the type of the bindTo value - switch (attribute.bindTo.type) { - case 'camunda:InputParameter': - case 'camunda:OutputParameter': - addAttributeValueToCamundaIO(element, bpmnFactory, attribute.bindTo.type, attribute, modeling)(attribute.value); - break; - case 'camunda:InputMapParameter': - addCamundaInputMapParameter(element.businessObject, attribute.name, attribute.value, bpmnFactory); - break; - case 'camunda:OutputMapParameter': - addCamundaOutputMapParameter(element.businessObject, attribute.name, attribute.value, bpmnFactory); - break; - case 'KeyValueMap': - addAttributeValueToKeyValueMap(element, attribute, bpmnFactory, commandStack)(attribute.value); - break; - default: - setAttributeValue(element, attribute, modeling)(attribute.value); - break; - } - }); + }); } /** @@ -122,11 +159,11 @@ export function handleConfigurationsAction(element, config, bpmnFactory, modelin * @returns {function(*): *} */ export function setAttributeValue(element, attribute, modeling) { - return (newValue) => { - return modeling.updateProperties(element, { - [attribute.bindTo.name]: newValue - }); - }; + return (newValue) => { + return modeling.updateProperties(element, { + [attribute.bindTo.name]: newValue, + }); + }; } /** @@ -137,17 +174,17 @@ export function setAttributeValue(element, attribute, modeling) { * @returns {(function(*): (*))|*} */ export function getAttributeValue(element) { - return (attribute) => { - const businessObject = getBusinessObject(element); - const realValue = businessObject.get(attribute.bindTo.name) || ''; - - // return the value of the property if defined or the value defined in the attribute - if (realValue && realValue !== '') { - return realValue; - } else { - return attribute.value; - } - }; + return (attribute) => { + const businessObject = getBusinessObject(element); + const realValue = businessObject.get(attribute.bindTo.name) || ""; + + // return the value of the property if defined or the value defined in the attribute + if (realValue && realValue !== "") { + return realValue; + } else { + return attribute.value; + } + }; } /** @@ -160,37 +197,44 @@ export function getAttributeValue(element) { * @param commandStack The commandStack of the current bpmn-js modeler. * @returns {(function(*): void)|*} */ -export function addAttributeValueToKeyValueMap(element, attribute, bpmnFactory, commandStack) { - return (value) => { - - const bo = element.businessObject; - - const businessObject = getBusinessObject(element); - const keyValueMap = bo.get(attribute.bindTo.name) || []; - - // add the value to the key value map - const existingEntry = keyValueMap.find((entry) => entry.name === attribute.name); - if (existingEntry) { - - // overwrite value of existing key value entry - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: existingEntry, - properties: {value: value}, - }); - } else { - - // create new key value entry - const param = bpmnFactory.create(dataConsts.KEY_VALUE_ENTRY, {name: attribute.name, value: value}); - - // add new entry to the key value map and save changes - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: {[attribute.bindTo.name]: keyValueMap.concat(param)}, - }); - } - }; +export function addAttributeValueToKeyValueMap( + element, + attribute, + bpmnFactory, + commandStack +) { + return (value) => { + const bo = element.businessObject; + + const businessObject = getBusinessObject(element); + const keyValueMap = bo.get(attribute.bindTo.name) || []; + + // add the value to the key value map + const existingEntry = keyValueMap.find( + (entry) => entry.name === attribute.name + ); + if (existingEntry) { + // overwrite value of existing key value entry + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: existingEntry, + properties: { value: value }, + }); + } else { + // create new key value entry + const param = bpmnFactory.create(dataConsts.KEY_VALUE_ENTRY, { + name: attribute.name, + value: value, + }); + + // add new entry to the key value map and save changes + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { [attribute.bindTo.name]: keyValueMap.concat(param) }, + }); + } + }; } /** @@ -201,18 +245,20 @@ export function addAttributeValueToKeyValueMap(element, attribute, bpmnFactory, * @returns {(function(*): (*))|*} */ export function getAttributeValueFromKeyValueMap(element) { - return (attribute) => { - const businessObject = getBusinessObject(element); - const keyValueMap = businessObject.get(attribute.bindTo.name) || []; - - // return value of respective key value entry or return the value of ConfigurationAttribute - const existingEntry = keyValueMap.find((entry) => entry.name === attribute.name); - if (existingEntry) { - return existingEntry.value; - } else { - return attribute.value; - } - }; + return (attribute) => { + const businessObject = getBusinessObject(element); + const keyValueMap = businessObject.get(attribute.bindTo.name) || []; + + // return value of respective key value entry or return the value of ConfigurationAttribute + const existingEntry = keyValueMap.find( + (entry) => entry.name === attribute.name + ); + if (existingEntry) { + return existingEntry.value; + } else { + return attribute.value; + } + }; } /** @@ -226,40 +272,48 @@ export function getAttributeValueFromKeyValueMap(element) { * @param modeling The modeling module of the current bpmn-js modeler. * @returns {(function(*): void)|*} */ -export function addAttributeValueToCamundaIO(element, bpmnFactory, camundaType, attribute, modeling) { - return (value) => { - - const businessObject = getBusinessObject(element); - const inputOutput = getCamundaInputOutput(businessObject, bpmnFactory); - - // create new io parameter with new value - const newParameter = bpmnFactory.create(camundaType, { - name: attribute.name, - value: value, - }); - - // Update existing or create a new io parameter - const parameters = camundaType === 'camunda:InputParameter' ? inputOutput.inputParameters : inputOutput.outputParameters; - let existingIoParameter = parameters.find((entry) => entry.name === attribute.name); - if (existingIoParameter) { - - // update existing value of io parameter - existingIoParameter.value = newParameter.value; - } else { +export function addAttributeValueToCamundaIO( + element, + bpmnFactory, + camundaType, + attribute, + modeling +) { + return (value) => { + const businessObject = getBusinessObject(element); + const inputOutput = getCamundaInputOutput(businessObject, bpmnFactory); + + // create new io parameter with new value + const newParameter = bpmnFactory.create(camundaType, { + name: attribute.name, + value: value, + }); - // create a new io parameter - existingIoParameter = bpmnFactory.create(camundaType, { - name: attribute.name, - value: attribute.value, - }); - parameters.push(existingIoParameter); - } + // Update existing or create a new io parameter + const parameters = + camundaType === "camunda:InputParameter" + ? inputOutput.inputParameters + : inputOutput.outputParameters; + let existingIoParameter = parameters.find( + (entry) => entry.name === attribute.name + ); + if (existingIoParameter) { + // update existing value of io parameter + existingIoParameter.value = newParameter.value; + } else { + // create a new io parameter + existingIoParameter = bpmnFactory.create(camundaType, { + name: attribute.name, + value: attribute.value, + }); + parameters.push(existingIoParameter); + } - // update model to propagate the new value - modeling.updateProperties(element, { - [camundaType]: existingIoParameter - }); - }; + // update model to propagate the new value + modeling.updateProperties(element, { + [camundaType]: existingIoParameter, + }); + }; } /** @@ -271,23 +325,30 @@ export function addAttributeValueToCamundaIO(element, bpmnFactory, camundaType, * @param camundaType The type of the camunda property, either camunda:InputMapParameter or camunda:OutputMapParameter. * @returns {(function(*): (*))|*} */ -export function getAttributeValueFromCamundaIO(element, bpmnFactory, camundaType) { - - return (attribute) => { - - const businessObject = getBusinessObject(element); - const inputOutput = getCamundaInputOutput(businessObject, bpmnFactory); - - const parameters = camundaType === 'camunda:InputParameter' ? inputOutput.inputParameters : inputOutput.outputParameters; - let existingInputParameter = parameters.find((entry) => entry.name === attribute.name); - - // return value of existing io parameter or the default value of the ConfigurationAttribute - if (existingInputParameter) { - return existingInputParameter.value; - } else { - return attribute.value; - } - }; +export function getAttributeValueFromCamundaIO( + element, + bpmnFactory, + camundaType +) { + return (attribute) => { + const businessObject = getBusinessObject(element); + const inputOutput = getCamundaInputOutput(businessObject, bpmnFactory); + + const parameters = + camundaType === "camunda:InputParameter" + ? inputOutput.inputParameters + : inputOutput.outputParameters; + let existingInputParameter = parameters.find( + (entry) => entry.name === attribute.name + ); + + // return value of existing io parameter or the default value of the ConfigurationAttribute + if (existingInputParameter) { + return existingInputParameter.value; + } else { + return attribute.value; + } + }; } /** @@ -304,18 +365,13 @@ export function getAttributeValueFromCamundaIO(element, bpmnFactory, camundaType * @returns {undefined|any} The icon object or undefined if no such property exists or the saved icon string is not correct formatted */ export function extractConfigSVG(element) { - const svgStr = element.businessObject.get(configsConsts.CONFIGURATIONS_ICON); - if (svgStr) { - try { - return JSON.parse(svgStr); - } catch (err) { - return undefined; - } + const svgStr = element.businessObject.get(configsConsts.CONFIGURATIONS_ICON); + if (svgStr) { + try { + return JSON.parse(svgStr); + } catch (err) { + return undefined; } - return undefined; + } + return undefined; } - - - - - diff --git a/components/bpmn-q/modeler-component/editor/configurations/Constants.js b/components/bpmn-q/modeler-component/editor/configurations/Constants.js index f192aad1..c9cc4740 100644 --- a/components/bpmn-q/modeler-component/editor/configurations/Constants.js +++ b/components/bpmn-q/modeler-component/editor/configurations/Constants.js @@ -1,6 +1,5 @@ - // constant used to save the id of the selected configuration in an element -export const SELECT_CONFIGURATIONS_ID = 'selectedConfigurationId'; +export const SELECT_CONFIGURATIONS_ID = "selectedConfigurationId"; // constant used to save the icon of a selected configuration in an element -export const CONFIGURATIONS_ICON = 'configsIcon'; \ No newline at end of file +export const CONFIGURATIONS_ICON = "configsIcon"; diff --git a/components/bpmn-q/modeler-component/editor/events/EditorEventHandler.js b/components/bpmn-q/modeler-component/editor/events/EditorEventHandler.js index 6fe4dab7..ebda19e0 100644 --- a/components/bpmn-q/modeler-component/editor/events/EditorEventHandler.js +++ b/components/bpmn-q/modeler-component/editor/events/EditorEventHandler.js @@ -11,7 +11,7 @@ let modelerComponent; * @param newModelerComponent The quantum workflow modeler component. */ export function initEditorEventHandler(newModelerComponent) { - modelerComponent = newModelerComponent; + modelerComponent = newModelerComponent; } /** @@ -24,14 +24,14 @@ export function initEditorEventHandler(newModelerComponent) { * not invoked, and false otherwise. */ export function dispatchWorkflowEvent(type, workflowXml, workflowName) { - const newEvent = new CustomEvent(type, { - detail: { - workflowName: workflowName, - workflow: workflowXml - }, - cancelable: true - }); - return modelerComponent?.dispatchEvent?.(newEvent)??true; + const newEvent = new CustomEvent(type, { + detail: { + workflowName: workflowName, + workflow: workflowXml, + }, + cancelable: true, + }); + return modelerComponent?.dispatchEvent?.(newEvent) ?? true; } /** @@ -42,5 +42,9 @@ export function dispatchWorkflowEvent(type, workflowXml, workflowName) { * @param callBckFunction The function defining the action executed when the event occurs */ export function addWorkflowEventListener(type, callBckFunction) { - modelerComponent.addEventListener(type, (event) => callBckFunction(event), false); -} \ No newline at end of file + modelerComponent.addEventListener( + type, + (event) => callBckFunction(event), + false + ); +} diff --git a/components/bpmn-q/modeler-component/editor/plugin/PluginConfigHandler.js b/components/bpmn-q/modeler-component/editor/plugin/PluginConfigHandler.js index a9b78c5f..55c9e550 100644 --- a/components/bpmn-q/modeler-component/editor/plugin/PluginConfigHandler.js +++ b/components/bpmn-q/modeler-component/editor/plugin/PluginConfigHandler.js @@ -25,9 +25,9 @@ let pluginConfigList = []; * @param pluginConfig List of plugin configurations. */ export function setPluginConfig(pluginConfig) { - pluginConfigList = pluginConfig || []; - console.log('New plugin config set: '); - console.log(pluginConfig); + pluginConfigList = pluginConfig || []; + console.log("New plugin config set: "); + console.log(pluginConfig); } /** @@ -38,8 +38,9 @@ export function setPluginConfig(pluginConfig) { * was defined for the plugin. */ export function getPluginConfig(pluginName) { - const plugin = pluginConfigList.find(element => element.name === pluginName) || {}; - return plugin.config || {}; + const plugin = + pluginConfigList.find((element) => element.name === pluginName) || {}; + return plugin.config || {}; } /** @@ -48,5 +49,5 @@ export function getPluginConfig(pluginName) { * @returns {*[]} The plugin configurations as an array. */ export function getAllConfigs() { - return pluginConfigList; -} \ No newline at end of file + return pluginConfigList; +} diff --git a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js index 61fa6075..0108c077 100644 --- a/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js +++ b/components/bpmn-q/modeler-component/editor/plugin/PluginHandler.js @@ -1,11 +1,11 @@ -import PlanQKPlugin from '../../extensions/planqk/PlanQKPlugin'; -import QuantMEPlugin from '../../extensions/quantme/QuantMEPlugin'; +import PlanQKPlugin from "../../extensions/planqk/PlanQKPlugin"; +import QuantMEPlugin from "../../extensions/quantme/QuantMEPlugin"; import OpenTOSCAPlugin from "../../extensions/opentosca/OpenTOSCAPlugin"; -import DataFlowPlugin from '../../extensions/data-extension/DataFlowPlugin'; -import QHAnaPlugin from '../../extensions/qhana/QHAnaPlugin'; -import { getAllConfigs } from './PluginConfigHandler'; -import GeneralTab from '../config/GeneralTab'; -import GitHubTab from '../../extensions/quantme/configTabs/GitHubTab'; +import DataFlowPlugin from "../../extensions/data-extension/DataFlowPlugin"; +import QHAnaPlugin from "../../extensions/qhana/QHAnaPlugin"; +import { getAllConfigs } from "./PluginConfigHandler"; +import GeneralTab from "../config/GeneralTab"; +import GitHubTab from "../../extensions/quantme/configTabs/GitHubTab"; /** * Handler for plugins of the modeler. Controls active plugins and the properties they define. Central access point to @@ -15,78 +15,83 @@ import GitHubTab from '../../extensions/quantme/configTabs/GitHubTab'; // list of plugins integrated in the modeler, register new plugins here // dependencies can be specified by the name of the corresponding plugins const PLUGINS = [ - { - plugin: QuantMEPlugin, - dependencies: ['DataFlowPlugin'] - }, - { - plugin: DataFlowPlugin, - dependencies: [] - }, - { - plugin: QHAnaPlugin, - dependencies: [] - }, - { - plugin: PlanQKPlugin, - dependencies: [] - }, - { - plugin: OpenTOSCAPlugin, - dependencies: [] - } + { + plugin: QuantMEPlugin, + dependencies: ["DataFlowPlugin"], + }, + { + plugin: DataFlowPlugin, + dependencies: [], + }, + { + plugin: QHAnaPlugin, + dependencies: [], + }, + { + plugin: PlanQKPlugin, + dependencies: [], + }, + { + plugin: OpenTOSCAPlugin, + dependencies: [], + }, ]; // list of currently active plugins in the current running instance of the modeler, defined based on the plugin configuration let activePlugins = []; export function getActivePlugins() { - if (activePlugins.length > 0) { - return activePlugins; - } else { - activePlugins = []; - - const loadPlugin = (plugin) => { - if (!activePlugins.includes(plugin.plugin)) { - for (const dependency of plugin.dependencies) { - const dependencyPlugin = PLUGINS.find((p) => p.plugin.name === dependency); - if (dependencyPlugin && !activePlugins.includes(dependencyPlugin.plugin)) { - activePlugins.push(dependencyPlugin.plugin); - loadPlugin(dependencyPlugin); - } - } - activePlugins.push(plugin.plugin); - } - }; - - for (const pluginConfig of getAllConfigs()) { - const plugin = PLUGINS.find( - (p) => p.plugin.name === pluginConfig.name && checkEnabledStatus(p.plugin.name) - ); - if (plugin) { - loadPlugin(plugin); - } + if (activePlugins.length > 0) { + return activePlugins; + } else { + activePlugins = []; + + const loadPlugin = (plugin) => { + if (!activePlugins.includes(plugin.plugin)) { + for (const dependency of plugin.dependencies) { + const dependencyPlugin = PLUGINS.find( + (p) => p.plugin.name === dependency + ); + if ( + dependencyPlugin && + !activePlugins.includes(dependencyPlugin.plugin) + ) { + activePlugins.push(dependencyPlugin.plugin); + loadPlugin(dependencyPlugin); + } } - - return activePlugins; + activePlugins.push(plugin.plugin); + } + }; + + for (const pluginConfig of getAllConfigs()) { + const plugin = PLUGINS.find( + (p) => + p.plugin.name === pluginConfig.name && + checkEnabledStatus(p.plugin.name) + ); + if (plugin) { + loadPlugin(plugin); + } } -} - + return activePlugins; + } +} export function checkEnabledStatus(pluginName) { - switch (pluginName) { - case 'dataflow': - return process.env.ENABLE_DATA_FLOW_PLUGIN !== "false"; - case 'planqk': - return process.env.ENABLE_PLANQK_PLUGIN !== "false"; - case 'qhana': - return process.env.ENABLE_QHANA_PLUGIN !== "false"; - case 'quantme': - return process.env.ENABLE_QUANTME_PLUGIN !== "false"; - case 'opentosca': - return process.env.ENABLE_OPENTOSCA_PLUGIN !== "false"; - } + switch (pluginName) { + case "dataflow": + return process.env.ENABLE_DATA_FLOW_PLUGIN !== "false"; + case "planqk": + return process.env.ENABLE_PLANQK_PLUGIN !== "false"; + case "qhana": + return process.env.ENABLE_QHANA_PLUGIN !== "false"; + case "quantme": + return process.env.ENABLE_QUANTME_PLUGIN !== "false"; + case "opentosca": + return process.env.ENABLE_OPENTOSCA_PLUGIN !== "false"; + } } /** * Returns all additional modules for the bpmn-js modeler the active plugins define in their extensionModule @@ -95,19 +100,18 @@ export function checkEnabledStatus(pluginName) { * @returns {*[]} Array of additional modules defined by the active plugins. */ export function getAdditionalModules() { + const modules = []; - const modules = []; - - // load all additional modules of the active plugins - for (let plugin of getActivePlugins()) { - if (plugin.extensionModule) { - modules.push(plugin.extensionModule); - } + // load all additional modules of the active plugins + for (let plugin of getActivePlugins()) { + if (plugin.extensionModule) { + modules.push(plugin.extensionModule); } + } - console.log('\n Get Additional Modules'); - console.log(modules); - return modules; + console.log("\n Get Additional Modules"); + console.log(modules); + return modules; } /** @@ -116,19 +120,18 @@ export function getAdditionalModules() { * @returns {*[]} Array of css style modules defined by the active plugins. */ export function getStyles() { + let styles = []; - let styles = []; - - // load css styles of the active plugins - for (let plugin of getActivePlugins()) { - if (plugin.styling) { - styles = styles.concat(plugin.styling); - } + // load css styles of the active plugins + for (let plugin of getActivePlugins()) { + if (plugin.styling) { + styles = styles.concat(plugin.styling); } + } - console.log('\n Get Plugin Styling'); - console.log(styles); - return styles; + console.log("\n Get Plugin Styling"); + console.log(styles); + return styles; } /** @@ -138,18 +141,18 @@ export function getStyles() { * @returns {*[]} Object containing the moddle extensions defined by the active plugins. */ export function getModdleExtension() { - const extensions = {}; + const extensions = {}; - // load all moddle extensions defined by the active plugins - for (let plugin of getActivePlugins()) { - if (plugin.moddleDescription) { - extensions[plugin.name] = plugin.moddleDescription; - } + // load all moddle extensions defined by the active plugins + for (let plugin of getActivePlugins()) { + if (plugin.moddleDescription) { + extensions[plugin.name] = plugin.moddleDescription; } + } - console.log('\n Get Moddle Extensions: '); - console.log(extensions); - return extensions; + console.log("\n Get Moddle Extensions: "); + console.log(extensions); + return extensions; } /** @@ -158,17 +161,17 @@ export function getModdleExtension() { * @returns {*[]} Array of css style modules defined by the active plugins. */ export function getTransformationButtons() { - const transformationButtons = []; + const transformationButtons = []; - // load all transformation buttons of the active plugins - for (let plugin of getActivePlugins()) { - if (plugin.transformExtensionButton) { - transformationButtons.push(plugin.transformExtensionButton); - } + // load all transformation buttons of the active plugins + for (let plugin of getActivePlugins()) { + if (plugin.transformExtensionButton) { + transformationButtons.push(plugin.transformExtensionButton); } + } - console.log('\n Got ' + transformationButtons.length + ' Transformations'); - return transformationButtons; + console.log("\n Got " + transformationButtons.length + " Transformations"); + return transformationButtons; } /** @@ -177,18 +180,18 @@ export function getTransformationButtons() { * @returns {*[]} Array of buttons defined by the active plugins. */ export function getPluginButtons() { - const pluginButtons = []; + const pluginButtons = []; - for (let plugin of getActivePlugins()) { - if (plugin.buttons) { - pluginButtons.push(plugin.buttons); - } + for (let plugin of getActivePlugins()) { + if (plugin.buttons) { + pluginButtons.push(plugin.buttons); } + } - console.log('\n Got ' + pluginButtons.length + ' Plugin Buttons'); - console.log(pluginButtons); + console.log("\n Got " + pluginButtons.length + " Plugin Buttons"); + console.log(pluginButtons); - return pluginButtons; + return pluginButtons; } /** @@ -198,27 +201,29 @@ export function getPluginButtons() { * @returns {*[]} Array of config tabs defined by the active plugins. */ export function getConfigTabs() { + // add default editor tab to configure editor configs + let configTabs = [ + { + tabId: "EditorTab", + tabTitle: "General", + configTab: GeneralTab, + }, + { + tabId: "GitHubTab", + tabTitle: "GitHub", + configTab: GitHubTab, + }, + ]; - // add default editor tab to configure editor configs - let configTabs = [{ - tabId: 'EditorTab', - tabTitle: 'General', - configTab: GeneralTab, - }, { - tabId: 'GitHubTab', - tabTitle: 'GitHub', - configTab: GitHubTab, - }]; - - // load the config tabs of the active plugins into one array - for (let plugin of getActivePlugins()) { - if (plugin.configTabs && checkEnabledStatus(plugin.name)) { - configTabs = configTabs.concat(plugin.configTabs); - } + // load the config tabs of the active plugins into one array + for (let plugin of getActivePlugins()) { + if (plugin.configTabs && checkEnabledStatus(plugin.name)) { + configTabs = configTabs.concat(plugin.configTabs); } + } - console.log('\n Got ' + configTabs.length + ' Config Tabs'); - console.log(configTabs); + console.log("\n Got " + configTabs.length + " Config Tabs"); + console.log(configTabs); - return configTabs; -} \ No newline at end of file + return configTabs; +} diff --git a/components/bpmn-q/modeler-component/editor/popup/CustomPopupMenu.js b/components/bpmn-q/modeler-component/editor/popup/CustomPopupMenu.js index 319a79ad..9a2209a5 100644 --- a/components/bpmn-q/modeler-component/editor/popup/CustomPopupMenu.js +++ b/components/bpmn-q/modeler-component/editor/popup/CustomPopupMenu.js @@ -1,197 +1,193 @@ import PopupMenu from "diagram-js/lib/features/popup-menu/PopupMenu"; -import {html, render} from 'diagram-js/lib/ui'; -import SearchablePopupMenuComponent from './SearchablePopupMenuComponent'; -import {getMoreOptions} from "../util/PopupMenuUtilities"; +import { html, render } from "diagram-js/lib/ui"; +import SearchablePopupMenuComponent from "./SearchablePopupMenuComponent"; +import { getMoreOptions } from "../util/PopupMenuUtilities"; /** * PopupMenu with a search bar that searches though all entries loaded in the menu including entries of MoreOptionEntries. * Extends the PopupMenu of diagram-js. */ export default class CustomPopupMenu extends PopupMenu { + constructor(config, eventBus, canvas) { + super(config, eventBus, canvas); - constructor(config, eventBus, canvas) { - super(config, eventBus, canvas); + // rerender if the selected element in the modeler changes + eventBus.on("element.changed", (event) => { + const element = this.isOpen() && this._current.element; - // rerender if the selected element in the modeler changes - eventBus.on('element.changed', event => { + if (event.element === element) { + this._render(); + } + }); + } + + /** + * Open a new popup menu at the given position to display menu entries based on the type of the given element. + * The entries of the menu will be determined by the providers registered for the given ID and the type of the + * given element. + * + * @param element The given element the popup menu is opened for. + * @param providerId Id string of the type of the providers defining the entries of the popup menu, e.g. 'bpmn-replace' + * @param position The position the menu should be displayed at + * @param options Options to configure the opened menu. + */ + open(element, providerId, position, options) { + this.openWithEntries(element, providerId, undefined, options, position); + } + + /** + * Open a new popup menu at the given position to display menu entries based on the type of the given element + * with the given menu entries in it. If newEntries is undefined, the menu entries be determined by the providers + * registered for the given ID and the type of the given element. + * + * @param element The element the popup menu is opened for. + * @param providerId Id string of the type of the providers defining the entries of the popup menu, e.g. 'bpmn-replace' + * @param newEntries Menu entries which should be displayed in this menu. + * @param position The position the menu should be displayed at + * @param options Options to configure the opened menu. + */ + openWithEntries( + element, + providerId, + newEntries, + options, + position = this._current.position + ) { + if (!element) { + throw new Error("Element is missing"); + } - const element = this.isOpen() && this._current.element; + if (!providerId) { + throw new Error("No registered providers for: " + providerId); + } - if (event.element === element) { - this._render(); - } - }); + if (!position) { + throw new Error("the position argument is missing"); } - /** - * Open a new popup menu at the given position to display menu entries based on the type of the given element. - * The entries of the menu will be determined by the providers registered for the given ID and the type of the - * given element. - * - * @param element The given element the popup menu is opened for. - * @param providerId Id string of the type of the providers defining the entries of the popup menu, e.g. 'bpmn-replace' - * @param position The position the menu should be displayed at - * @param options Options to configure the opened menu. - */ - open(element, providerId, position, options) { - this.openWithEntries(element, providerId, undefined, options, position); + if (this.isOpen()) { + this.close(); } - /** - * Open a new popup menu at the given position to display menu entries based on the type of the given element - * with the given menu entries in it. If newEntries is undefined, the menu entries be determined by the providers - * registered for the given ID and the type of the given element. - * - * @param element The element the popup menu is opened for. - * @param providerId Id string of the type of the providers defining the entries of the popup menu, e.g. 'bpmn-replace' - * @param newEntries Menu entries which should be displayed in this menu. - * @param position The position the menu should be displayed at - * @param options Options to configure the opened menu. - */ - openWithEntries(element, providerId, newEntries, options, position = this._current.position) { - if (!element) { - throw new Error('Element is missing'); - } - - if (!providerId) { - throw new Error('No registered providers for: ' + providerId); - } - - if (!position) { - throw new Error('the position argument is missing'); - } - - if (this.isOpen()) { - this.close(); - } - - // load entries from providers registered for providerId for the type of element - let { - entries, - headerEntries - } = this._getContext(element, providerId); - - // use predefined entries to fill the menu if defined - if (newEntries) { - entries = newEntries; - } - - // update state of the menu - this._current = { - position, - className: providerId, - element, - entries, - headerEntries, - container: this._createContainer({provider: providerId}), - options - }; - - this._emit('open'); - - this._bindAutoClose(); + // load entries from providers registered for providerId for the type of element + let { entries, headerEntries } = this._getContext(element, providerId); - this._render(); + // use predefined entries to fill the menu if defined + if (newEntries) { + entries = newEntries; } - /** - * Get the menu entry with the given ID of the current menu entries. The current menu entries also contain the elements - * defined in MoreOptionEntries - * - * @param entryId The ID of the searched menu entry - * @returns {*} The menu entry or an error if no menu entry with this ID exists. - * @private - */ - _getEntry = function(entryId) { - - const extendedOptionsEntries = []; - - // create list of all entries of the menu including the entries of MoreOptionEntries - for (let [key, value] of Object.entries(this._current.entries)) { - value.id = key; - extendedOptionsEntries.push(value); - - // get entries of the MoreOptionsEntry - const moreOptions = getMoreOptions(value); - - if (Array.isArray(moreOptions)) { - extendedOptionsEntries.push(...moreOptions); - } else { - extendedOptionsEntries.push(moreOptions); - } - } - - // convert list to object with properties for each entry - const entries = {}; - for (let entry of extendedOptionsEntries) { - entries[entry.id] = entry; - } - - const entry = entries[entryId] || this._current.headerEntries[entryId]; - - if (!entry) { - throw new Error('entry not found'); - } - - return entry; + // update state of the menu + this._current = { + position, + className: providerId, + element, + entries, + headerEntries, + container: this._createContainer({ provider: providerId }), + options, }; - /** - * Render the popup menu - * - * @private - */ - _render() { - - const { - position: _position, - className, - entries, - headerEntries, - options - } = this._current; - - // load current entries to display them in the menu - const entriesArray = Object.entries(entries).map( - ([key, value]) => ({id: key, ...value}) - ); - - // load current header entries to display them in the menu - const headerEntriesArray = Object.entries(headerEntries).map( - ([key, value]) => ({id: key, ...value}) - ); - - const position = _position && ( - (container) => this._ensureVisible(container, _position) - ); - - const scale = this._updateScale(this._current.container); - - const onClose = result => this.close(result); - const onSelect = (event, entry, action) => this.trigger(event, entry, action); - - render( - html` - <${SearchablePopupMenuComponent} - onClose=${onClose} - onSelect=${onSelect} - position=${position} - className=${className} - entries=${entriesArray} - headerEntries=${headerEntriesArray} - scale=${scale} - onOpened=${this._onOpened.bind(this)} - onClosed=${this._onClosed.bind(this)} - ...${{...options}} - /> - `, - this._current.container - ); + this._emit("open"); + + this._bindAutoClose(); + + this._render(); + } + + /** + * Get the menu entry with the given ID of the current menu entries. The current menu entries also contain the elements + * defined in MoreOptionEntries + * + * @param entryId The ID of the searched menu entry + * @returns {*} The menu entry or an error if no menu entry with this ID exists. + * @private + */ + _getEntry = function (entryId) { + const extendedOptionsEntries = []; + + // create list of all entries of the menu including the entries of MoreOptionEntries + for (let [key, value] of Object.entries(this._current.entries)) { + value.id = key; + extendedOptionsEntries.push(value); + + // get entries of the MoreOptionsEntry + const moreOptions = getMoreOptions(value); + + if (Array.isArray(moreOptions)) { + extendedOptionsEntries.push(...moreOptions); + } else { + extendedOptionsEntries.push(moreOptions); + } + } + + // convert list to object with properties for each entry + const entries = {}; + for (let entry of extendedOptionsEntries) { + entries[entry.id] = entry; } + + const entry = entries[entryId] || this._current.headerEntries[entryId]; + + if (!entry) { + throw new Error("entry not found"); + } + + return entry; + }; + + /** + * Render the popup menu + * + * @private + */ + _render() { + const { + position: _position, + className, + entries, + headerEntries, + options, + } = this._current; + + // load current entries to display them in the menu + const entriesArray = Object.entries(entries).map(([key, value]) => ({ + id: key, + ...value, + })); + + // load current header entries to display them in the menu + const headerEntriesArray = Object.entries(headerEntries).map( + ([key, value]) => ({ id: key, ...value }) + ); + + const position = + _position && ((container) => this._ensureVisible(container, _position)); + + const scale = this._updateScale(this._current.container); + + const onClose = (result) => this.close(result); + const onSelect = (event, entry, action) => + this.trigger(event, entry, action); + + render( + html` + <${SearchablePopupMenuComponent} + onClose=${onClose} + onSelect=${onSelect} + position=${position} + className=${className} + entries=${entriesArray} + headerEntries=${headerEntriesArray} + scale=${scale} + onOpened=${this._onOpened.bind(this)} + onClosed=${this._onClosed.bind(this)} + ...${{ ...options }} + /> + `, + this._current.container + ); + } } -CustomPopupMenu.$inject = [ - 'config.popupMenu', - 'eventBus', - 'canvas', -]; \ No newline at end of file +CustomPopupMenu.$inject = ["config.popupMenu", "eventBus", "canvas"]; diff --git a/components/bpmn-q/modeler-component/editor/popup/HiddenTextFieldEntry.js b/components/bpmn-q/modeler-component/editor/popup/HiddenTextFieldEntry.js index 21d5ec32..61b4d3d8 100644 --- a/components/bpmn-q/modeler-component/editor/popup/HiddenTextFieldEntry.js +++ b/components/bpmn-q/modeler-component/editor/popup/HiddenTextFieldEntry.js @@ -1,5 +1,5 @@ -import {TextFieldEntry} from '@bpmn-io/properties-panel'; -import React from '@bpmn-io/properties-panel/preact/compat'; +import { TextFieldEntry } from "@bpmn-io/properties-panel"; +import React from "@bpmn-io/properties-panel/preact/compat"; /** * Entry which can be used in a properties group of the properties panel. Allows the definition of a TextFieldEntry @@ -16,16 +16,27 @@ import React from '@bpmn-io/properties-panel/preact/compat'; * @return {JSX.Element} * @constructor */ -export function HiddenTextFieldEntry({id, element, label, getValue, setValue, debounce, hidden}) { - - return <> - {!hidden() && ()} - ; -} \ No newline at end of file +export function HiddenTextFieldEntry({ + id, + element, + label, + getValue, + setValue, + debounce, + hidden, +}) { + return ( + <> + {!hidden() && ( + + )} + + ); +} diff --git a/components/bpmn-q/modeler-component/editor/popup/SearchablePopupMenuComponent.js b/components/bpmn-q/modeler-component/editor/popup/SearchablePopupMenuComponent.js index e97d9435..a03b20bf 100644 --- a/components/bpmn-q/modeler-component/editor/popup/SearchablePopupMenuComponent.js +++ b/components/bpmn-q/modeler-component/editor/popup/SearchablePopupMenuComponent.js @@ -1,22 +1,19 @@ import { - useEffect, - useRef, - useState, - useLayoutEffect, - useMemo, - useCallback, - html -} from 'diagram-js/lib/ui'; - -import { - closest as domClosest, - matches as domMatches -} from 'min-dom'; - -import PopupMenuList from 'diagram-js/lib/features/popup-menu/PopupMenuList'; -import classNames from 'clsx'; -import {isDefined} from 'min-dash'; -import {getMoreOptions} from "../util/PopupMenuUtilities"; + useEffect, + useRef, + useState, + useLayoutEffect, + useMemo, + useCallback, + html, +} from "diagram-js/lib/ui"; + +import { closest as domClosest, matches as domMatches } from "min-dom"; + +import PopupMenuList from "diagram-js/lib/features/popup-menu/PopupMenuList"; +import classNames from "clsx"; +import { isDefined } from "min-dash"; +import { getMoreOptions } from "../util/PopupMenuUtilities"; /** * A component that renders the popup menus with a search bar that allows to search MoreOptionsEntries. @@ -25,161 +22,177 @@ import {getMoreOptions} from "../util/PopupMenuUtilities"; * */ export default function SearchablePopupMenuComponent(props) { - const { - onClose, - onSelect, - className, - headerEntries, - position, - title, - width, - scale, - search, - entries: originalEntries, - onOpened, - onClosed - } = props; - - const searchable = useMemo(() => { - if (!isDefined(search)) { - return false; - } - - return originalEntries.length > 5; - }, [search, originalEntries]); - - const inputRef = useRef(); - - const [value, setValue] = useState(''); - - // collect MoreOptionEntries - const extendedOptionsEntries = originalEntries.flatMap(function (entry) { - return getMoreOptions(entry); - }); - - // filter menu entries based on the value - const filterEntries = useCallback((entriesToFilter, value) => { - - if (!searchable || !value || value === '') { - return originalEntries; - } - - const filter = entry => { - if (!value) { - return (entry.rank || 0) >= 0; - } - - const search = [ - entry.description || '', - entry.label || '', - entry.search || '' - ] - .join('---') - .toLowerCase(); - - return value - .toLowerCase() - .split(/\s/g) - .every(term => search.includes(term)); - }; - - return entriesToFilter.filter(filter); - }, [searchable]); - - const [entries, setEntries] = useState(filterEntries(originalEntries, value)); - const [selectedEntry, setSelectedEntry] = useState(entries[0]); - - const updateEntries = useCallback((newEntries) => { - - // select first entry if non is selected - if (!selectedEntry || !newEntries.includes(selectedEntry)) { - setSelectedEntry(newEntries[0]); - } - - setEntries(newEntries); - }, [selectedEntry, setEntries, setSelectedEntry]); - - // filter entries on value change - useEffect(() => { - updateEntries(filterEntries(extendedOptionsEntries, value)); - }, [value, originalEntries]); - - // register global handler - useEffect(() => { - const handleKeyDown = event => { - if (event.key === 'Escape') { - event.preventDefault(); - - return onClose(); - } - }; - - document.documentElement.addEventListener('keydown', handleKeyDown); - - return () => { - document.documentElement.removeEventListener('keydown', handleKeyDown); - }; - }, []); - - // focus input on initial mount - useLayoutEffect(() => { - inputRef.current && inputRef.current.focus(); - }, []); - - // handle keyboard selection - const keyboardSelect = useCallback(direction => { - const idx = entries.indexOf(selectedEntry); - - let nextIdx = idx + direction; - - if (nextIdx < 0) { - nextIdx = entries.length - 1; - } - - if (nextIdx >= entries.length) { - nextIdx = 0; + const { + onClose, + onSelect, + className, + headerEntries, + position, + title, + width, + scale, + search, + entries: originalEntries, + onOpened, + onClosed, + } = props; + + const searchable = useMemo(() => { + if (!isDefined(search)) { + return false; + } + + return originalEntries.length > 5; + }, [search, originalEntries]); + + const inputRef = useRef(); + + const [value, setValue] = useState(""); + + // collect MoreOptionEntries + const extendedOptionsEntries = originalEntries.flatMap(function (entry) { + return getMoreOptions(entry); + }); + + // filter menu entries based on the value + const filterEntries = useCallback( + (entriesToFilter, value) => { + if (!searchable || !value || value === "") { + return originalEntries; + } + + const filter = (entry) => { + if (!value) { + return (entry.rank || 0) >= 0; } - setSelectedEntry(entries[nextIdx]); - }, [entries, selectedEntry, setSelectedEntry]); - - const handleKeyDown = useCallback(event => { - if (event.key === 'Enter' && selectedEntry) { - return onSelect(event, selectedEntry); - } - - // ARROW_UP or SHIFT + TAB navigation - if (event.key === 'ArrowUp' || (event.key === 'Tab' && event.shiftKey)) { - keyboardSelect(-1); - - return event.preventDefault(); - } - - // ARROW_DOWN or TAB navigation - if (event.key === 'ArrowDown' || event.key === 'Tab') { - keyboardSelect(1); - - return event.preventDefault(); - } - }, [onSelect, onClose, selectedEntry, keyboardSelect]); - - const handleKey = useCallback(event => { - if (domMatches(event.target, 'input')) { - setValue(() => event.target.value); - } - }, [setValue]); + const search = [ + entry.description || "", + entry.label || "", + entry.search || "", + ] + .join("---") + .toLowerCase(); + + return value + .toLowerCase() + .split(/\s/g) + .every((term) => search.includes(term)); + }; + + return entriesToFilter.filter(filter); + }, + [searchable] + ); + + const [entries, setEntries] = useState(filterEntries(originalEntries, value)); + const [selectedEntry, setSelectedEntry] = useState(entries[0]); + + const updateEntries = useCallback( + (newEntries) => { + // select first entry if non is selected + if (!selectedEntry || !newEntries.includes(selectedEntry)) { + setSelectedEntry(newEntries[0]); + } + + setEntries(newEntries); + }, + [selectedEntry, setEntries, setSelectedEntry] + ); + + // filter entries on value change + useEffect(() => { + updateEntries(filterEntries(extendedOptionsEntries, value)); + }, [value, originalEntries]); + + // register global handler + useEffect(() => { + const handleKeyDown = (event) => { + if (event.key === "Escape") { + event.preventDefault(); + + return onClose(); + } + }; - useEffect(() => { - onOpened(); + document.documentElement.addEventListener("keydown", handleKeyDown); - return () => { - onClosed(); - }; - }, []); + return () => { + document.documentElement.removeEventListener("keydown", handleKeyDown); + }; + }, []); + + // focus input on initial mount + useLayoutEffect(() => { + inputRef.current && inputRef.current.focus(); + }, []); + + // handle keyboard selection + const keyboardSelect = useCallback( + (direction) => { + const idx = entries.indexOf(selectedEntry); + + let nextIdx = idx + direction; + + if (nextIdx < 0) { + nextIdx = entries.length - 1; + } + + if (nextIdx >= entries.length) { + nextIdx = 0; + } + + setSelectedEntry(entries[nextIdx]); + }, + [entries, selectedEntry, setSelectedEntry] + ); + + const handleKeyDown = useCallback( + (event) => { + if (event.key === "Enter" && selectedEntry) { + return onSelect(event, selectedEntry); + } + + // ARROW_UP or SHIFT + TAB navigation + if (event.key === "ArrowUp" || (event.key === "Tab" && event.shiftKey)) { + keyboardSelect(-1); + + return event.preventDefault(); + } + + // ARROW_DOWN or TAB navigation + if (event.key === "ArrowDown" || event.key === "Tab") { + keyboardSelect(1); + + return event.preventDefault(); + } + }, + [onSelect, onClose, selectedEntry, keyboardSelect] + ); + + const handleKey = useCallback( + (event) => { + if (domMatches(event.target, "input")) { + setValue(() => event.target.value); + } + }, + [setValue] + ); + + useEffect(() => { + onOpened(); + + return () => { + onClosed(); + }; + }, []); - const displayHeader = useMemo(() => title || headerEntries.length > 0, [title, headerEntries]); + const displayHeader = useMemo( + () => title || headerEntries.length > 0, + [title, headerEntries] + ); - return html` + return html` <${PopupMenuWrapper} onClose=${onClose} onKeyup=${handleKey} @@ -189,58 +202,87 @@ export default function SearchablePopupMenuComponent(props) { width=${width} scale=${scale} > - ${displayHeader && html` + ${ + displayHeader && + html`
-

${title}

- ${headerEntries.map(entry => html` - onSelect(event, entry)} - title=${entry.title || entry.label} - data-id=${entry.id} - onMouseEnter=${() => setSelectedEntry(entry)} - onMouseLeave=${() => setSelectedEntry(null)} - > - ${entry.imageUrl ? html` - - ` : null} - - ${entry.label ? html` - ${entry.label} - ` : null} - - `)} +

${title}

+ ${headerEntries.map( + (entry) => html` + onSelect(event, entry)} + title=${entry.title || entry.label} + data-id=${entry.id} + onMouseEnter=${() => setSelectedEntry(entry)} + onMouseLeave=${() => setSelectedEntry(null)} + > + ${entry.imageUrl + ? html` + + ` + : null} + ${entry.label + ? html` + ${entry.label} + ` + : null} + + ` + )}
- `} - ${originalEntries.length > 0 && html` + ` + } + ${ + originalEntries.length > 0 && + html`
- - ${searchable && html` - - `} - - <${PopupMenuList} - entries=${entries} - selectedEntry=${selectedEntry} - setSelectedEntry=${setSelectedEntry} - onAction=${onSelect} - /> + ${searchable && + html` + + `} + + <${PopupMenuList} + entries=${entries} + selectedEntry=${selectedEntry} + setSelectedEntry=${setSelectedEntry} + onAction=${onSelect} + />
- ${entries.length === 0 && html` -
No matching entries found.
+ ${entries.length === 0 && + html` +
+ No matching entries found. +
`} - `} + ` + } `; } @@ -251,79 +293,78 @@ export default function SearchablePopupMenuComponent(props) { * @param {*} props */ function PopupMenuWrapper(props) { - const { - onClose, - onKeydown, - onKeyup, - className, - children, - position: positionGetter - } = props; - - const popupRef = useRef(); - - const checkClose = useCallback((event) => { - - const popup = domClosest(event.target, '.djs-popup', true); - - if (popup) { - return; - } - - onClose(); - }, [onClose]); - - useLayoutEffect(() => { - if (typeof positionGetter !== 'function') { - return; - } - - const popupEl = popupRef.current; - const position = positionGetter(popupEl); - - popupEl.style.left = `${position.x}px`; - popupEl.style.top = `${position.y}px`; - }, [popupRef.current, positionGetter]); - - // focus popup initially, on mount - useLayoutEffect(() => { - popupRef.current && popupRef.current.focus(); - }, []); - - return html` -
-
- ${children} -
-
- `; + const { + onClose, + onKeydown, + onKeyup, + className, + children, + position: positionGetter, + } = props; + + const popupRef = useRef(); + + const checkClose = useCallback( + (event) => { + const popup = domClosest(event.target, ".djs-popup", true); + + if (popup) { + return; + } + + onClose(); + }, + [onClose] + ); + + useLayoutEffect(() => { + if (typeof positionGetter !== "function") { + return; + } + + const popupEl = popupRef.current; + const position = positionGetter(popupEl); + + popupEl.style.left = `${position.x}px`; + popupEl.style.top = `${position.y}px`; + }, [popupRef.current, positionGetter]); + + // focus popup initially, on mount + useLayoutEffect(() => { + popupRef.current && popupRef.current.focus(); + }, []); + + return html` +
+
+ ${children} +
+
+ `; } // helpers ////////////////////// function getPopupStyle(props) { - return { - transform: `scale(${props.scale})`, - width: `${props.width}px` - }; + return { + transform: `scale(${props.scale})`, + width: `${props.width}px`, + }; } function getHeaderClasses(entry, selected) { - return classNames( - 'entry', - entry.className, - entry.active ? 'active' : '', - entry.disabled ? 'disabled' : '', - selected ? 'selected' : '' - ); -} \ No newline at end of file + return classNames( + "entry", + entry.className, + entry.active ? "active" : "", + entry.disabled ? "disabled" : "", + selected ? "selected" : "" + ); +} diff --git a/components/bpmn-q/modeler-component/editor/popup/index.js b/components/bpmn-q/modeler-component/editor/popup/index.js index da86ab88..6c023892 100644 --- a/components/bpmn-q/modeler-component/editor/popup/index.js +++ b/components/bpmn-q/modeler-component/editor/popup/index.js @@ -1,6 +1,6 @@ import CustomPopupMenu from "./CustomPopupMenu"; export default { - __init__: ["popupMenu"], - popupMenu: ["type", CustomPopupMenu], -}; \ No newline at end of file + __init__: ["popupMenu"], + popupMenu: ["type", CustomPopupMenu], +}; diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/bpmn-fonts.css b/components/bpmn-q/modeler-component/editor/resources/styling/bpmn-fonts.css index 031f2105..024c54fa 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/bpmn-fonts.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/bpmn-fonts.css @@ -1,16 +1,18 @@ @font-face { - font-family: 'bpmn'; - src: url('./fontello/font/bpmn.eot'); - src: url('./fontello/font/bpmn.eot') format('embedded-opentype'), - url('./fontello/font/bpmn.svg') format('svg'); - font-weight: normal; - font-style: normal; + font-family: "bpmn"; + src: url("./fontello/font/bpmn.eot"); + src: url("./fontello/font/bpmn.eot") format("embedded-opentype"), + url("./fontello/font/bpmn.svg") format("svg"); + font-weight: normal; + font-style: normal; } @font-face { - font-family: 'bpmn'; - src: url('data:application/octet-stream;base64,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') format('woff'), - url('data:application/octet-stream;base64,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AADoDAAAAAwAAOgNAADoDQAAAA0AAOgOAADoDgAAAA4AAOgPAADoDwAAAA8AAOgRAADoEQAAABAAAOgSAADoEgAAABEAAOgTAADoEwAAABIAAOgUAADoFAAAABMAAOgVAADoFQAAABQAAOgWAADoFgAAABUAAOgXAADoFwAAABYAAOgYAADoGAAAABcAAOgZAADoGQAAABgAAOgaAADoGgAAABkAAOgbAADoGwAAABoAAOgcAADoHAAAABsAAOgdAADoHQAAABwAAOgeAADoHgAAAB0AAOgfAADoHwAAAB4AAOggAADoIAAAAB8AAOghAADoIQAAACAAAOgiAADoIgAAACEAAOgjAADoIwAAACIAAOgkAADoJAAAACMAAOglAADoJQAAACQAAOgmAADoJgAAACUAAOgnAADoJwAAACYAAOgoAADoKAAAACcAAOgpAADoKQAAACgAAOgqAADoKgAAACkAAOgrAADoKwAAACoAAOgsAADoLAAAACsAAOgtAADoLQAAACwAAOguAADoLgAAAC0AAOgvAADoLwAAAC4AAOgxAADoMQAAAC8AAOgyAADoMgAAADAAAOgzAADoMwAAADEAAOg0AADoNAAAADIAAOg1AADoNQAAADMAAOg2AADoNgAAADQAAOg3AADoNwAAADUAAOg4AADoOAAAADYAAOg5AADoOQAAADcAAOg6AADoOgAAADgAAOg7AADoOwAAADkAAOg8AADoPAAAADoAAOg9AADoPQAAADsAAOg+AADoPgAAADwAAOg/AADoPwAAAD0AAOhAAADoQAAAAD4AAOhBAADoQQAAAD8AAOhCAADoQgAAAEAAAOhDAADoQwAAAEEAAOhEAADoRAAAAEIAAOhFAADoRQAAAEMAAOhGAADoRgAAAEQAAOhHAADoRwAAAEUAAOhIAADoSAAAAEYAAOhJAADoSQAAAEcAAOhKAADoSgAAAEgAAOhLAADoSwAAAEkAAOhMAADoTAAAAEoAAOhNAADoTQAAAEsAAOhOAADoTgAAAEwAAOhPAADoTwAAAE0AAOhQAADoUAAAAE4AAOhRAADoUQAAAE8AAOhSAADoUgAAAFAAAOhTAADoUwAAAFEAAOhUAADoVAAAAFIAAOhVAADoVQAAAFMAAOhWAADoVgAAAFQAAOhXAADoVwAAAFUAAOhYAADoWAAAAFYAAOhZ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BAQJBAQEBQIBAQQEAxUDBAoHGQMKAgMDAwMDAwMCBgMCAwIDAggBnAUEBAEHChwLBxIECwQHBAQHBAgEEAgEAwICAQQEAwQFDAUNBAUEBQgFJgQJBAQEBAQEAQQDAQMBAwQECAQJBAICAQIGAwQF/kQEBQQGAgIBAgMDBQQBBA0JAQ0BBAUFBAEEBQUKBRkBBAEUBgUFBQsKCQUEBAMEAQIDAwQHEwUEBQUEBQUEJR4IBAQJDAMNAhIFBAUDARQEBAkIFiwFBQQFBQUEBRsEBgQBAQIDAwQEBAkDBgULBQUFBhQBHgUKBQUEAQQBBAUcDQQBBAUDAgECAgMECAH6/sgBODFaPrq6fQHyAQEBAQcCAgQDBAQECQQFBgMCCQIBAQEBAQECBQECAwMIBAUECQMDAwcBAQEBUwECAgQHCQkKAQkHBgcDBAMEAwQHCAcUBA0EBCADAwICAgIDAwgJBQYFBQQBBAEEBRwNBAEIBAEEBAQIAQMBAwEDBAgDAQMEAwEGBAcIAQQEAQMEAwEDDAMBAwEDAQgEBAQBBAQEAQQNFwUFBAEEAQkFCgUMBAUEBAMEAQIFBwweCAQJBBQHCAcEAwQDBAMHBgQDBwUEBAkEBAMEAQICBQwFCQQDBgIBAQEBAQICBwICAwICAgECAwMIBQkIAwUCAQIBAgECAgIJAgEBAScCAwUCBAECDwMCAQIJDAkBCQQDAwEDAwEDCwcHCQkEAwQEAQEEAwwDBAYDAwIDAwMDAwMCAwMCAwIDAgMCAwQDFgMDCAkJBwMEOwECAwMEDQULAgMDAwMDAwIDAwojAwcDBwQOCwMHAwgDAwIBAQMCBAgJBAIECQQEBAQEBQMBAwEDBAEDEAQDAQMIAwEKAQMDAQMDAQMDAQYECQEDAgMDqQEEBAMECAQECQUECQ0EGwUNCQQECQQBBCIEBQEEBAMDAgECAgMECAkFBBIECwQHBAQHBAgEHgQIBAcEBAcEBAkEBAQDBC0BBAgEBAQEHgUKBQUFBQkFJwUEBQkKBDIEBQQIBAYCAgYICgUZAQQBFAYFBQULBiYFBgULBgsGFgYJCAMDBBACBggICQYRBQYFBgsFIQYLBQUKBg8BBAEZBQoBBAYCAQEDAgQICQgBKQQFBQQFBQkFIgUJBQUFBQoFFwUEBwICOQECAwMIBwQHBBoDCwsHBgQDBxQPAwcBCAUEBQgDAwICAgIHAQMVAQMLAQcQBAQMBAQEBAQECQQmBQUIBAQDBKABBAMDBAkJCQYBDAEGDAYFAwMCAQQBBAECBQQCAgEBAQQBAQEGAQEBAgEBAgMHBQQFBAgDAwkCAQYDEAMGAwIDAgMCAwIDBgIDAwMDAwdKAQIBAwYMAwMFAwIBAQEEAwMECQUECAMDAgEBAQECCQICAgECAQIBAQECAgIBAQEEAQEBBQQDBQQFCAgDAgEnAQEDBgQJBQQEBAIFAwQJAQYJAwIBAgMCAQIDBAMKAggBAgECAgMBAgMDCAUECQQDAwMEAgECAQIBAgEQEAYCAwUJAwoBAgICDwIDBQYMFAECAQIBAgECBgIGCQQFBAQEAwMCAQEBAgICAQIBCAwDBAMCAQIDAgECEgEJBAMFBAQEBQkEAwMEAAAABQAA/5YDvQMjABYAMQA6AD4ARAAAASYOAxYXHgI3PgE3Njc2Jy4BJyYHNhcWFxYXFgYHBgcGBwYnJicmJyYnJjc+AhMGDwEXNRcRBycUFSclFBUnJicB9FimgUQBQD07obBRVIYnKQMGHh52Tl5rVlJQOjsXFxcrLERGWlhWWENDJyYDAyMecpQ/Nmwv0crKGI8BWSpDIgMiAUZ+obOmPj9JCR4delFTWFdVU4cmLzEBJiVDRFRPp0hKLzQQDxYWOTZQT1daTklwPv8AJk0hlI+PASiPYGRlZGVlZR4wFwAJAAD/ogO4AyAACwAXAFIAuwFEAaEB6AIyAp4AAAEVIxUzFTM1MzUjNQczFTMVIxUjNSM1MxMrAQcjByMHFQ8FFR8FMz8DMzczNzM3MxczFzMXMx8BMz8DNS8EIzUjJyMnFw8GFR8KMx8GFR8CMx8NMz8ENS8BNScjLwYjLwE1Iy8LIy8BIy8HIzUnNS8EIwUPASMPBBUPBBUHIwcVDwMjFQcVByMVDwQVByMPARUPAhUPAiMPAxUPAxUHFQ8KFQcVHwQ/BDM/BDU/JjUvAwEPBRUXFQcVBxUHFQcVBxUHFQcVBxUHFQcVBxUPBxUfBD8FMz8BMz8BNT8BMzczNzU3NTM1NzU3NTc1NzU3NTc1NzU3NSc1LwQFDwUVFxUXFRczHwUzHwEVFxUXFRcVMxcVFxUfBD8ENS8NNSc1JzUnNS8DAQ8XHwQ/BDM/AzM3MzczNzM3Mzc1NzM3NTczPwIzNzM1PwM1LwQFDwUVHwMVHwEzHwIzHwIVHwEzHwEVHwEzFzMXMxczHwIzFzMXFTMfBzMXFRczFTMXMz8ENS8EIycjJyMnIy8KIy8EIy8FIwGufX2MfX11Xn19Xn19MwcNBg0NBycNDgQDAwQBAgMDCQQFBgwGFwYLBgYGBikGBgYGBgUfBAUMAwMCAgYDCicGBwYNB+kFBAQDAwECAgIGAgIDBgUEAwgBDgIIAgYCBAECAgECAwIDBQQDBAMKBQYDCQQJBAQDBAIEBgEEAwIDAgMCAQIIAQUDBAIEAgIFAgcCAgECCQECAgMHAwIDAgECAwIGAgoF/fMFBwECAwIDAgMCAwIDBAECAgMCBAEEBAECAgICAgIBAgICBAICBAEBAQIDAgIKAQICAgEEAQIBAgECAQICAQQEAw4ECAQDAQECAwECAgIBAgECAQIBAgIDAgECCAIBAgICAQQCBAEGAgoCBgMKAwYDBQMCAQQGCAUCsgQFAwYCAQEBAQEBAQECAQMFAwUDBAMCBgMBAgMGBAkJBAQDAgEBAwIBAgMDAgEFAQwDAQIBAQEBAQEBAQQDBAQI/KYEBQMEBAEBAwIBAwIBBAEKAQQDAgMCAQIDAQIEBwkJBAQDBAIEAgMCBwQBBgECAQIIAQEBAQUICAKxBAQJBAUKBA8FBQsPBgULBSEGCxAIBAUCAgQDBA0JBgYUBQESBgcFAQUBBQEFARwBCwUBBQUBBQsPAQQBAwMCAQQDBAQI/d4EBAQDAgIBAgMEDQIBAgMCAQIDAwsCAQgDAwUBAgECAQIBAgMDAQIBAgECBAMGAw0DHQIDDQQDDQkFCAUCAQQDBAQDBAgDFAIRAwkFEQIGBQMFBQUCAQ8CBQICAQQDBBEEBAUCIX2MfX2MfRd9Xn1+XQGTAQIGAQIEAgMECAkFBAQDBAECAgIEAgEBAQEBBQEGBAMJBQgIAgQGAQEBTwEBAwMEBAQFCQQGAQIBBgMEAggOAwgDBgMEAQICAwQDBAMHBwMIAxIKCAIEAgIDAwgKBwcBDAgEBAQEBAQECwEHAwYCBgIDBQMHAwIDCQMCAgcCAgICAQEBAgIEAgUMAQQCAgICAgECAgICAgEEAgECAgMEAQQBBAECAwIDAgECAwIBAgYCAQIGAwMCBQIBAg8DAgECAQIDBgMDAwMDAwMDAQQJBQgDAwMBBAMDBAYFAwIF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OBoJFhhQYwkUHQkJBAgECQ0EWVkSJg4/BA4NCgUHBgICAwgIBAEIBCYECAMEBAQHBAgDGgMIBwcDCQUEBAQDBAEEAwcBAwUEBAQEBAkEJgQJBQQEBQ2xBQMGBwEDCAMBFQMBAwMBCQEMAQkJAwMCAQIDAwgEAQQCAgIDAwgEBQQJAwgDAgQDAgMCAwIDAgMDAwIDAwMDAwMDAwYDIwIDBAQCAgUECAUBhAQEBAMDAwEGAwMCAwIDAgMCAwcWAQQBBAEGAwICBAQDBAUJBAQEAwQBAgICAQIBAgECAgIBAQEIAQEBAgIEAQIHAwIBAgMCAQIGAwkDAwMECf3HBAgDAwICAQIBAQEBAQECAQgCAQEBAwMEBAQFCQQDAwQBAgUDAgEBAQEBAQIBAQIDAwkEAo0FBAQDBAICBwIDBQIEAQIDDAoDBAIEAQEEAwQECQgFBwMPAgEGBQgBAQEGAQIBAgECAgIJAQICAwQIBf2uBAQEAwICAgUGDAYBDAEGBwMBAwMBBwcDAQcHAQMBAwQIBAQMBAQEBAkEBAQFAgEBBAQDFQMECgcZAwoCAwMDAwMDAwIGAwIDAgMCCA4BpQQEAQcKHAsHEgQLBAcEBAcECAQQCAQDAgIBBAQDBAURDQQFBAUIBSYECQQEBAQEBAEEAwEDAQMEBAgECQQCAgECBgMEBQSnBQYGCwUMBQUGJgYKCAQCAwEBAgYECQQrBQkFBQUFCgUeBQoFBQUFCgQYCQUEAwQEAQEEAwQHJgUGBQsGCwbvBAUDBAQCAggCDAcIBwQDBAMEAwcGBw8CCQIDEQQDBAkJBAQDBAECAwIBAgMCAQISAQkEBgEDBAMBAwgEAwEDAQMBCAQEBAEECAEECgj99AQIBQQBBAQEDAQEBAgDAQMEAwQDBAkBDwIBAgMDBQIBBAMDAQIDAwgECQUHBQUQBgIGAg8HBgcDBAMEAwQHCAQEBwIBAgIDBAgEAq8ECAYCAQEBAQECCAIBAgIEARYBAQQDDAUJBAYFBAMKAggBAgECAgIHAQEBAQEEAwQIBPymBAQIBAIBAQECBgICAgEEAQYCCgMEAQMGBAUJBAQEAwQCARIBAgECAQICAgcCAQEBAQIHAwUElAUEBgICAQIDAwUEAQQNCQENAQQFBQQBBAUFCgUZAQQBFAYFBQULCgkFBAQDBAECAwMEBxMFBAUFBAUFBCUeCAQECQwDDQ0CGgQFAwEUBAQJCBYsBQUEBQUFBAUbBAYEAQECAwMEBAQJAwYFCwUFBQYUAR4FCgUFBAEEAQQFHA0EAQQFAwIBAgIDBAgEAjQBSzs5gFkeFRVtQD5xGhUnASI9SUw/KAcaOE1RISUsMhIjNgEPEQMDAQYZIkQI7QECAgUMBQkEAwYCAQEBAQECAgcCAgMCAgIBAgMDCAUJCAMFAgECAQIBAgICCQIBAQEnAQEDBQIEAQIPAwIBAgkMCQEJBAMDAQMDAQMLBwcJCQQDBAQBAQQDDAMEBgMDAgMDAwMDAwIDAwIDAgMCAwIDBAMWAwMICQkHAwQ8AQEDAwQNBQsCAwMDAwMDAgMDCiMDBwMHBA4LAwcDCAMDAgEBAwIECAkEAgQJBAQEBAQFAwEDAQMEAQMQBAMBAwgDAQoBAwMBAwMBAwMBBgQJAQMCAwMCqwEEBAMECAQECQUECQ0EGwUNCQQECQQBBCIEBQEEBAMDAgECAgMECAkFBBIECwQHBAQHBAgEHgQIBAcEBAcEBAkEBAQDBAF3AQIDAwgHBAcEGgMLCwcGBAMHFA8DBwEIBQQFCAMDAgICAgcBAxUBAwsBBxAEBAwEBAQEBAQJBCYFBQgEBAMEoQICAwMECQkJBgEMAQYMBgUDAwIBBAEEAQIFBAICAQEBBAEBAQYBAQECAQECAwcFBAUECAMDCQIBBgMQAwYDAgMCAwIDAgMGAgMDAwMDBwNNAQIBAwYMAwMFAwIBAQEEAwMECQUECAMDAgEBAQECCQICAgECAQIBAQECAgIBAQEEAQEBBQQDBQQFCAgDAgECzgEBAQEHAgIEAwQEBAkEBQYDAgkCAQEBAQEBAgUBAgMDCAQFBAkDAwMHAQEBAVMBAgIEBwkJCgEJBwYHAwQDBAMEBwgHFAQNBAQgAwMCAgICAwMICQUGBQUEAQQBBAUcDQQBCAQBBAQECAEDAQMBAwQIAwEDBAMBBgQHAgoBBAQBAwQDAQMMAwEDAQMBCAQEBAEEBAQBBA0XBQUEAQQBCQUKBQwEBQQEAwQBAgUHDB4IBAkEFAcIBwQDBAMEAwcGBAMHBQQECQQEAwT+xQEECAQEBAQeBQoFBQUFCQUnBQQFCQoEMgQFBAgEBgICBggKBRkBBAEUBgUFBQsGJgUGBQsGCwYWBgkIAwMEEAEBBggICQYRBQYF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BwMEAwQDBAcIBAQHAgECAgMECAEEEg0KBQcGAgIDCAgEAQgEJgQIAwQEBAcECAMaAwgHBwMJBQQEBAMEAQQDBwEDBQQEBAQECQQmBAkFBAQFDa0ECAYHAQMIAwEVAwEDAwEJAQwBCQkDAwIBAgMDCAQBBAICAgMDCAQFBAkDCAMCBAMCAwIDAgMCAwMDAgMDAwMDAwMDBgMjAgMEBAICBQQIAYQFCAQDAwMBBgMDAgMCAwIDAgMHFgEEAQQBBgMCAgQEAwQFCQQEBAMEAQICAgECAQIBAgICAQEBCAEBAQICBAECBwMCAQIDAgECBgMJAwMDBP3DBQQIAwMCAgECAQEBAQEBAgEIAgEBAQMDBAQEBQkEAwMEAQIFAwIBAQEBAQECAQECAwMJAu4FBAgGAgEBAQEBAggCAQICBAEWAQEEAwwFCQQGBQQDCgIIAQIBAgICBwEBAQEBBAMECPynBQgIBAIBAQECBgICAgEEAQYCCgMEAQMGBAUJBAQEAwQCARIBAgECAQICAgcCAQEBAQIHAwUC8AQFBAQDBAICBwIDBQIEAQIDDAoDBAIEAQEEAwQECQgFBwMPAgEGBQgBAQEGAQIBAgECAgIJAQICAwQI/a0ECAQDAgICBQYMBgEMAQYHAwEDAwEHBwMBBwcBAwEDBAgEBAwEBAQECQQEBAUCAQEEBAMVAwQKBxkDCgIDAwMDAwMDAgYDAgMCAwIIAZwFBAQBBwocCwcSBAsEBwQEBwQIBBAIBAMCAgEEBAMEBQwFDQQFBAUIBSYECQQEBAQEBAEEAwEDAQMEBAgECQQCAgECBgMEBf5EBAUEBgICAQIDAwUEAQQNCQENAQQFBQQBBAUFCgUZAQQBFAYFBQULCgkFBAQDBAECAwMEBxMFBAUFBAUFBCUeCAQECQwDDQISBQQFAwEUBAQJCBYsBQUEBQUFBAUbBAYEAQECAwMEBAQJAwYFCwUFBQYUAR4FCgUFBAEEAQQFHA0EAQQFAwIBAgIDBAgCIX2MfX2MfRd9Xn1+XQGVAQEBAQcCAgQDBAQECQQFBgMCCQIBAQEBAQECBQECAwMIBAUECQMDAwcBAQEBUwECAgQHCQkKAQkHBgcDBAMEAwQHCAcUBA0EBCADAwICAgIDAwgJBQYFBQQBBAEEBRwNBAEIBAEEBAQIAQMBAwEDBAgDAQMEAwEGBAcIAQQEAQMEAwEDDAMBAwEDAQgEBAQBBAQEAQQNFwUFBAEEAQkFCgUMBAUEBAMEAQIFBwweCAQJBBQHCAcEAwQDBAMHBgQDBwUEBAkEBAMEAQICBQwFCQQDBgIBAQEBAQICBwICAwICAgECAwMIBQkIAwUCAQIBAgECAgIJAgEBAScCAwUCBAECDwMCAQIJDAkBCQQDAwEDAwEDCwcHCQkEAwQEAQEEAwwDBAYDAwIDAwMDAwMCAwMCAwIDAgMCAwQDFgMDCAkJBwMEOwECAwMEDQULAgMDAwMDAwIDAwojAwcDBwQOCwMHAwgDAwIBAQMCBAgJBAIECQQEBAQEBQMBAwEDBAEDEAQDAQMIAwEKAQMDAQMDAQMDAQYECQEDAgMDqQEEBAMECAQECQUECQ0EGwUNCQQECQQBBCIEBQEEBAMDAgECAgMECAkFBBIECwQHBAQHBAgEHgQIBAcEBAcEBAkEBAQDBC0BBAgEBAQEHgUKBQUFBQkFJwUEBQkKBDIEBQQIBAYCAgYICgUZAQQBFAYFBQULBiYFBgULBgsGFgYJCAMDBBACBggICQYRBQYFBgsFIQYLBQUKBg8BBAEZBQoBBAYCAQEDAgQICQgBKQQFBQQFBQkFIgUJBQUFBQoFFwUEBwICOQECAwMIBwQHBBoDCwsHBgQDBxQPAwcBCAUEBQgDAwICAgIHAQMVAQMLAQcQBAQMBAQEBAQECQQmBQUIBAQDBKABBAMDBAkJCQYBDAEGDAYFAwMCAQQBBAECBQQCAgEBAQQBAQEGAQEBAgEBAgMHBQQFBAgDAwkCAQYDEAMGAwIDAgMCAwIDBgIDAwMDAwdKAQIBAwYMAwMFAwIBAQEEAwMECQUECAMDAgEBAQECCQICAgECAQIBAQECAgIBAQEEAQEBBQQDBQQFCAgDAgEnAQEDBgQJBQQEBAIFAwQJAQYJAwIBAgMCAQIDBAMKAggBAgECAgMBAgMDCAUECQQDAwMEAgECAQIBAgEQEAYCAwUJAwoBAgICDwIDBQYMFAECAQIBAgECBgIGCQQFBAQEAwMCAQEBAgICAQIBCAwDBAMCAQIDAgECEgEJBAMFBAQEBQkEAwMEAAAAAAgAAP+aA7gDIgAUACkALgAyADYAOgA+AEIAAAEiBwYHBhQXFhcWMjc2NzY0JyYnJgcyFxYXFhQHBgcGIicmJyY0NzY3NgcVESERBTMRIxMVMzUHFTM1BxUzNQcVMzUB9HpqZjw+PjxmavRqZjw+PjxmanpuXls1Nzc1W17cXls1Nzc1W14pAS7+6v7+GsrKysrKysoDIj48Zmr0amY8Pj48Zmr0amY8PjE3NVte3F5bNTc3NVte3F5bNTfVDP6QAXwY/rQBJxkZShgYVRgYTRgYAAAACgAA/6IDuAMgAA8AHwAxAGwA1QFeAbsCAgJMArgAAAEmBgcGHgI3PgE3NiYnJgc2HgIOAy4CNjc+ARcGDwEOARYXFj4BOwE1IzY3JgMrAQcjByMHFQ8FFR8FMz8DMzczNzM3MxczFzMXMx8BMz8DNS8EIzUjJyMnFw8GFR8KMx8GFR8CMx8NMz8ENS8BNScjLwYjLwE1Iy8LIy8BIy8HIzUnNS8EIwUPASMPBBUPBBUHIwcVDwMjFQcVByMVDwQVByMPARUPAhUPAiMPAxUPAxUHFQ8KFQcVHwQ/BDM/BDU/JjUvAwEPBRUXFQcVBxUHFQcVBxUHFQcVBxUHFQcVBxUPBxUfBD8FMz8BMz8BNT8BMzczNzU3NTM1NzU3NTc1NzU3NTc1NzU3NSc1LwQFDwUVFxUXFRczHwUzHwEVFxUXFRcVMxcVFxUfBD8ENS8NNSc1JzUnNS8DAQ8XHwQ/BDM/AzM3MzczNzM3Mzc1NzM3NTczPwIzNzM1PwM1LwQFDwUVHwMVHwEzHwIzHwIVHwEzHwEVHwEzFzMXMxczHwIzFzMXFTMfBzMXFRczFTMXMz8ENS8EIycjJyMnIy8KIy8EIy8FIwH0QG8XGRhWfjo9UQEERDkrMiZIMxMOK0FQSzgaCRYYUGMJFB0JCQQIBAkNBFlZEiYOOwcNBg0NBycNDgQDAwQBAgMDCQQFBgwGFwYLBgYGBikGBgYGBgUfBAUMAwMCAgYDCicGBwYNB+kFBAQDAwECAgIGAgIDBgUEAwgBDgIIAgYCBAECAgECAwIDBQQDBAMKBQYDCQQJBAQDBAIEBgEEAwIDAgMCAQIIAQUDBAIEAgIFAgcCAgECCQECAgMHAwIDAgECAwIGAgoF/fMFBwECAwIDAgMCAwIDBAECAgMCBAEEBAECAgICAgIBAgICBAICBAEBAQIDAgIKAQICAgEEAQIBAgECAQICAQQEAw4ECAQDAQECAwECAgIBAgECAQIBAgIDAgECCAIBAgICAQQCBAEGAgoCBgMKAwYDBQMCAQQGCAUCsgQFAwYCAQEBAQEBAQECAQMFAwUDBAMCBgMBAgMGBAkJBAQDAgEBAwIBAgMDAgEFAQwDAQIBAQEBAQEBAQQDBAQI/KYEBQMEBAEBAwIBAwIBBAEKAQQDAgMCAQIDAQIEBwkJBAQDBAIEAgMCBwQBBgECAQIIAQEBAQUICAKxBAQJBAUKBA8FBQsPBgULBSEGCxAIBAUCAgQDBA0JBgYUBQESBgcFAQUBBQEFARwBCwUBBQUBBQsPAQQBAwMCAQQDBAQI/d4EBAQDAgIBAgMEDQIBAgMCAQIDAwsCAQgDAwUBAgECAQIBAgMDAQIBAgECBAMGAw0DHQIDDQQDDQkFCAUCAQQDBAQDBAgDFAIRAwkFEQIGBQMFBQUCAQ8CBQICAQQDBBEEBAUCNAFLOzmAWR4VFW1APnEaFScBIj1JTD8oBxo4TVEhJSwyEiM2AQ8RAwMBBhkiRAgBSAECBgECBAIDBAgJBQQEAwQBAgICBAIBAQEBAQUBBgQDCQUICAIEBgEBAU8BAQMDBAQEBQkEBgECAQYDBAIIDgMIAwYDBAECAgMEAwQDBwcDCAMSCggCBAICAwMICgcHAQwIBAQEBAQEBAsBBwMGAgYCAwUDBwMCAwkDAgIHAgICAgEBAQICBAIFDAEEAgICAgIBAgICAgIBBAIBAgIDBAEEAQQBAgMCAwIBAgMCAQIGAgECBgMDAgUCAQIPAwIBAgECAwYDAwMDAwMDAwEECQUIAwMDAQQDAwQGBQMCBQECAwIDAgMCAwMCBQIDAgwCAwIDAgMEAwQDBgMKAwYCCgEGAgUEBAQJCQYEAf7GAQICCAQEBAQzAwkDBgIGAwMDAwMDBgIDAwkCDgMIAQ0FCwUGCgcEBQkEBgICAgIDAwMDBgYGBQEFBw8mAwoDAwcDAwQDAwQDAwQDBwMOAywFCQgEAgMCEwECAwMIBBsGBxMHDRMGBw0GHw0FAQUBBQEFAQUBBQEBBAQEAgICAwMJCQgGBgUGEAsGEQUGBgUvBgUGBgwGBwUHBgL+sgECBgQEBgQJBAMFCQIDBAMMAQQDBAMHCQkIAwMDAgIBBgIGAwIDAgMDDwYBAwMBAwQHDAQBAwQEBAkIBAMCAgIBAwIEBAkEBQQEAwEKAgICAgICAQEHAgUBAQEDAgIBAgECAQEBAQECAgIEAgkBAQQBBAEEBwUECQgEAgMBAwcGAwMGAgIDAQMCAwIJAgMCAQQBBA0BAgAAAwAAAAADIAJYAAMABwALAAATFSE1BRUhNQUVITXIAlj9qAJY/agCWAJYZGTIZGTIZGQAAAUAAP/TA9QC6QAcAFYAdgCKAJ4AAAEiBgc5ARQXFhcGBwYPARUhNScmJyYnNjU5ATQmBzIzFxYXFh8BFhcWMzEyPwEWFRQHBgcXFhcWFRYVFA8BDgEiJi8BJjU0NzQ3Nj8BJy4BNTQ3MjczNgcwMQYXFhceATI2NzY3NicwMRYXFSM1IxUjNSMVIzU2EyIOARURFB4BMyEyPgE1ETQuASMFITIeARURFA4BIyEiLgE1ETQ+AQFIJTIBCgYJIhceDwEBTAEPHhchFzFJAgIEDwkGBgIFAwYKDAoEBgUGEgIGCgIBAQEJHiQeCQEBAQIKBQIECg4IAwECCSICAQIGDCYqJgwGAgECMhIwFJwVLxMONVk1NVk1Ajo1WjQ0WjX9xgI6JT4kJD4l/cYlPiQkPgJjMSMSFg4LDA8UHAKFhQIcFA8MFyojMS8BAQICBAEEAQIFAwwPFg0PCxIBBAYGBAkCAgEKCgoKAQICCQQGBgQBFAMIIg4REAEDggwGDAUNDQ0NBQwGDBYgbExMTExsIgFLNFk1/m41WTQ0WTUBkjVZNDwkPSX+biU9JCQ9JQGSJT0kAAAFAAAAAAOPApMAAwAHAAsADwATAAATESERBTMVIzchFSEHMxUjNyEVIVkDNvztn5/CAi790sKfn8ICLv3SApP9lwJp0a2trSOlpaUAAwAAAAADUgK8AAQACAAUAAATFREhEQUhESEBFSMVMxUzNTM1IzWWArz9cAJk/ZwBBZubWpubArwZ/V0CvCz9nAH6m1qbm1qbAAAAAAQAAP+WA70DIgAXADMAPwBLAAABIg4DFhceAjc+ATc2NzYnLgEnJiMXMhcWFxYXFgYHBgcGBwYnJicmJyYnJjc+AhcHFSMVMxUzNTM1IzUHMxUzFSMVIzUjNTMB8Felf0QBQD07obBRVIYnKQMGHh52Tl5rCFVQTjk6FhcXKyxERlpYVlhDQycmAwMjHnKUT0Z9fYx9fXVefX1efX0DIkZ9obOmPj9JCR4delFTWFdVU4cmLzEnJkJEUk+nSEovNBAPFhY5NVFPV1pOSXA+AdB9jH19jH0XfV59fl0AAAAABAAA/5YDvQMjABYAMQA7AEQAAAEmDgMWFx4CNz4BNzY3NicuAScmBzYXFhcWFxYGBwYHBgcGJyYnJicmJyY3PgIXBg8BJwM3NjcXAxYXFhc3BycHAfRYpoFEAUA9O6GwUVSGJykDBh4edk5ea1ZSUDo7FxcXKyxERlpYVlhDQycmAwMjHnKU/BEiM4B0F0gjhokHDkMiHh18MgMiAUZ+obOmPj9JCR4delFTWFdVU4cmLzEBJiVDRFRPp0hKLzQQDxYWOTZQT1daTklwPtMjRWmm/qoeWy6aAQYJElUqQ3SRTQAABgAA/5UDvgMkABsAMQBJAGAAZABpAAABJgcGBwYHBhYXFhceATc+Ajc2Jy4BJyYnIyYHNhcWFxYXFgYHDgEmJy4CNzY3PgEXJgcGBwYHBhcWFxYXFjY3PgInJicuAQc2FxYXFgcWBgcGBwYmJyYnJjY3Njc2FwYHIQMWFyE2AfNsZWFBQgsMNzw9U0+3VliETwYGHhx3T1BXAxUQY11ZOjwICT0+QKa0TVBkIBUXOTiiX1tVUTI1AQQkJEFEUk2eP0FFBB4gPDB8QlBKRyssAQFIPkBLR4wzNhQVFCcnPUpVYGABgMBkM/7SMwMiAjY1Wl1sXLZJSigoCxwdd6RZW1NThigpBQIxAzEwU1ZjWKxAQzsQKyyPs1VZQkROKwMwL09SW1NQTjY4EBAlMDKMoklMMiswMAIsKkZKUEuLLC8HCTEzNUVCkDs9IitjrK0BJrRaWgAAAAAGAAD/lQO+AyQAGwAxAEkAYABqAHUAAAEmBwYHBgcGFhcWFx4BNz4CNzYnLgEnJicjJgc2FxYXFhcWBgcOASYnLgI3Njc+ARcmBwYHBgcGFxYXFhcWNjc+AicmJy4BBzYXFhcWBxYGBwYHBiYnJicmNjc2NzYFBg8BJwM3NjcXAxYXFhc3BycHBgcB82xlYUFCCww3PD1TT7dWWIRPBgYeHHdPUFcDFRBjXVk6PAgJPT5AprRNUGQgFRc5OKJfW1VRMjUBBCQkQURSTZ4/QUUEHiA8MHxCUEpHKywBAUg+QEtHjDM2FBUUJyc9SgECESIzgHQXSCOGiQcOQyIeHXwOGQsDIgI2NVpdbFy2SUooKAscHXekWVtTU4YoKQUCMQMxMFNWY1isQEM7ECssj7NVWUJETisDMC9PUltTUE42OBAQJTAyjKJJTDIrMDACLCpGSlBLiywvBwkxMzVFQpA7PSIrdiNFaab+qh5bLpoBBgkSVSpDdJEWJBMAAAMAAP+WA78DIgAbADIAOwAAASIHBgcGBwYWFxYXHgE3Njc+ATc2Jy4BJyYnJgc2FxYXFhcWBgcGBwYmJy4BNjc2Nz4BFwYPARc1FxEHAetsY2A/QAoLOT0+VE60U1dAQ1AIBx0beVFTWQ0PT0hHKywBA0A5PEhGkDc5MhAmKEAjUhk2bC/RysoDIjc2W11rXLdHSicmCxwdOzmhWFtTVYspKgQBjgIpKEVHT0mJLjEMDikwMYeXPkEjFRaiJk0hlI+PASiPAAAABAAA//ADqwLMABMAKwAvADwAABMiDgEVERQeATMhMj4BNRE0LgEjBSEyHgEVERQOASsBESERIyIuATURND4BEyERITcdASMVMxUzNTM1IzXYKkcqKkcqAjkqRykpRyr9xwI5HTEcHDEddP6ucx0xHBwxrwEU/uxzTU0uTU0CzClHKv5YKkcpKUcqAagqRykwHDEd/lgdMRwBNP7MHDEdAagdMRz+mf7s7Rc2Lk1NLk0AAAAABAAA//ADqwLMABMAKwAvADMAABMiDgEVERQeATMhMj4BNRE0LgEjBSEyHgEVERQOASsBESERIyIuATURND4BEyERITcVMzXXKkcpKUcqAjoqRykpRyr9xgI6HTEcHDEddP6udB0wHBwwsAEU/uwlywLMKUcq/lgqRykpRyoBqCpHKTAcMR3+WB0xHAE0/swcMR0BqB0xHP6Z/uunLy8AAAIAAP/wA6sCzAATACcAABMiDgEVERQeATMhMj4BNRE0LgEjBSEyHgEVERQOASMhIi4BNRE0PgHXKkcpKUcqAjoqRykpRyr9xgI6HTEcHDEd/cYdMBwcMALMKUcq/lgqRykpRyoBqCpHKTAcMR3+WB0xHBwxHQGoHTEcAAADAAD/lgO/AyIAGwAyADwAAAEiBwYHBgcGFhcWFx4BNzY3PgE3NicuAScmJyYHNhcWFxYXFgYHBgcGJicuATY3Njc+ARcGDwEnAzc2NxcB62xjYD9ACgs5PT5UTrRTV0BDUAgHHRt5UVNZDQ9PSEcrLAEDQDk8SEaQNzkyECYoQCNS1hEiM4B0F0gjhgMiNzZbXWtct0dKJyYLHB07OaFYW1NViykqBAGOAikoRUdPSYkuMQwOKTAxh5c+QSMVFnUjRWmm/qoeWy6aAAAGAAD/lQO+AyQAGwAxAEkAYABrAHkAAAEmBwYHBgcGFhcWFx4BNz4CNzYnLgEnJicjJgc2FxYXFhcWBgcOASYnLgI3Njc+ARcmBwYHBgcGFxYXFhcWNjc+AicmJy4BBzYXFhcWBxYGBwYHBiYnJicmNjc2NzYXBgIHNjc2NxcmLwEWHwEnBgcGBwY3Njc2AfNsZWFBQgsMNzw9U0+3VliETwYGHhx3T1BXAxUQY11ZOjwICT0+QKa0TVBkIBUXOTiiX1tVUTI1AQQkJEFEUk2eP0FFBB4gPDB8QlBKRyssAQFIPkBLR4wzNhQVFCcnPUpVGE0XFyonFH0ZMTIMFiJECBkSBgoBCxQWAyICNjVaXWxctklKKCgLHB13pFlbU1OGKCkFAjEDMTBTVmNYrEBDOxArLI+zVVlCRE4rAzAvT1JbU1BONjgQECUwMoyiSUwyKzAwAiwqRkpQS4ssLwcJMTM1RUKQOz0iK29C/vNBFzArFYdOnlEkSW5KBx0UBwsGIUNMAAAAAAcAAP+SA8EDIwAcADcAUQBqAHsAjACeAAABIiMiBwYHBgcGFhcWFx4BNzY3PgE3Ni4BJyYnJgcyMzYXFhcWFxYGBwYHDgEnJicmJyY3Njc+ARciIyIHBgcGBwYWFxYXFjY3Njc+AScmJy4BBzIzMhcWFxYXFgYHBgcGLgEnLgE3Njc+ARciBgcGHgI3PgE3NiYnJiMXMh4CDgMuAjY3PgEfAQYPAQ4BFhcWPgE7ATUjNjcmAfMBAmNeW0BCFhUeLi9ISLJZXEtKaBUWG15ISVY4OwUFXFZUOToPECo0NUpMtFNVOz0dHAkKLjW4bgUFVE9LMjQJCzY4OkxInUVGLTAeFBU2M45PAwNLRUMrLAYINzU4RUSPdiAiAh8hOihjLz9sFxkYVn46PVEBBEQ5KzIEJUYyEw4rQVBLOBoJFhhQLDcJFB0JCQQIBAkNBFlZEiYOAyIuLU5QYVazTU4zNSoODzAukVZXrZs2NxQOMQEtK0xNW1KoREYnKgogIUA/V1RZW0pXaSsrKkhJVE+dOzwbGw0lJj9Ao09SOzpDMCcmQEFLR4syNBIUG1U+QJZCRSsfImFKOzmAWR4VFW1APnEaFScjPEhMPygHGjhNUSElLAExEiM2AQ8RAwMBBhkiRAgAAAAEAAD/lgO9AyMAFgAxADwASgAAASYOAxYXHgI3PgE3Njc2Jy4BJyYHNhcWFxYXFgYHBgcGBwYnJicmJyYnJjc+AhcGAgc2NzY3FyYvARYfAScGBwYHBjc2NzYB9FimgUQBQD07obBRVIYnKQMGHh52Tl5rVlJQOjsXFxcrLERGWlhWWENDJyYDAyMecpRPGE0XFyonFH0ZMTIMFiJECBkSBgoBCxQWAyIBRn6hs6Y+P0kJHh16UVNYV1VThyYvMQEmJUNEVE+nSEovNBAPFhY5NlBPV1pOSXA+zEL+80EXMCsVh06eUSRJbkoHHRQHCwYhQ0wABAAA/5YDvQMjABYAMQA1ADoAAAEmDgMWFx4CNz4BNzY3NicuAScmBzYXFhcWFxYGBwYHBgcGJyYnJicmJyY3PgIXBgchAxYXITYB9FimgUQBQD07obBRVIYnKQMGHh52Tl5rVlJQOjsXFxcrLERGWlhWWENDJyYDAyMecpRPYGABgMBkM/7SMwMiAUZ+obOmPj9JCR4delFTWFdVU4cmLzEBJiVDRFRPp0hKLzQQDxYWOTZQT1daTklwPsCsrQEmtFpaAAcAAP/TA9QC6QATACcAKwAzADcAOwA/AAATIg4BFREUHgEzITI+ATURNC4BIwUhMh4BFREUDgEjISIuATURND4BFxEhEQUhFSE1IxUjFTMVIzchFSEDFSE11zVZNTVZNQI6NVo0NFo1/cYCOiU+JCQ+Jf3GJT4kJD4BAfr+GwHP/qkWYmJieAFX/ql2Ac0C6TRZNf5uNVk0NFk1AZI1WTQ8JD0l/m4lPSQkPSUBkiU9JEj+hAF8g2hnZxVmZmYBTmxsAAACAAAAAAN9ApMASgCzAAABIgc5AQYHOQEGBzkBBgcGHQEUFzAxFhcWMxY3OQEyNjU0JzMyNzY3MTYnPgE0Jy4BKwE2NTkBNCcuASMFNzY/ATY3PgEnLgEvATEHMDIVMhc5ARYGBzkBBg8BDgEVFBcxHgEXFjsBFjczMhYXFhQHBisBFSEyFhcWFTEUBiMhFSEyFhcWBzkBDgEjIRUzMhYXFhU5ARQHDgEjBic5ASInJic1Jj0BNDc2NzY3MTY3MTYzNDMBzA0SPbgvEwgCAQgQLShB3NsaHQUQGg4NBgYMGxsOCBgOFQYPCBgO/sMFBgQOEwYPBA0GEgoFAQEFBQUCBwgdDBYKAgIFBwMCDEuWcQYIBAgHBA/yAT4GCAQHCg/+wgEPBwkECAMDCwv+8c0FBgMGBgIGBtvbOR4hDQcBAgYRJMMxBwQBApMKKIAgPxggEyQNMSJBHhoBASMcDxEOCxgeFwMhOxQKDQ4RHRMLDAEFBwMOFQcTLhIKDAIBIwEIBxoJCh0MFQ0GAwYFBgEBAQEEBQwfCQYjBAUKEhMMIwYGDRUMCSMDBAgRDwgDAgEBFBY0AR4sCiQSHRUzGYcgBAEAAAMAAAAAA4oCagADAAYACwAAExEhEQUhBSUFJREhXwMr/SgChP6+/o4BcgFz/RsCav3pAhcj1Mr09P45AAIAAP/TA9QC6QATACMAABMiDgEVERQeATMhMj4BNRE0LgEjBSEyFhURFAYjISImNRE0Nuc5YTk5YTkCGjlhOTlhOf3mAhorOjor/eYrOjoC6ThfOf6KOV84OF85AXY5XzhuOSn+iik5OSkBdik5AAAFAAD/mgO4AyIAFAApADoASwBdAAABIgcGBwYUFxYXFjI3Njc2NCcmJyYHMhcWFxYUBwYHBiInJicmNDc2NzYXIgYHBh4CNz4BNzYmJyYjFzIeAg4DLgI2Nz4BHwEGDwEOARYXFj4BOwE1IzY3JgH0empmPD4+PGZq9GpmPD4+PGZqem5eWzU3NzVbXtxeWzU3NzVbXmo/bBcZGFZ+Oj1RAQREOSsyBCVGMhMOK0FQSzgaCRYYUCw3CRQdCQkECAQJDQRZWRImDgMiPjxmavRqZjw+PjxmavRqZjw+MTc1W17cXls1Nzc1W17cXls1N71KOzmAWR4VFW1APnEaFScjPEhMPygHGjhNUSElLAExEiM2AQ8RAwMBBhkiRAgAAAAFAAD/lgO9AyMAFgAxADYAQABGAAABJg4DFhceAjc+ATc2NzYnLgEnJgc2FxYXFhcWBgcGBwYHBicmJyYnJicmNz4CBxQVIREFMjMGBwYHBgcmNxQVITUXAfRYpoFEAUA9O6GwUVSGJykDBh4edk5ea1ZSUDo7FxcXKyxERlpYVlhDQycmAwMjHnKUfgGa/sFycg8gGA0VEEDj/sicAyIBRn6hs6Y+P0kJHh16UVNYV1VThyYvMQEmJUNEVE+nSEovNBAPFhY5NlBPV1pOSXA++JycATgxCRwWCQ8CNAVdXbp9AAAEAAD/lQO+AyMAGwAzAEoAYQAAASYHBgcGBwYWFxYXHgE3PgI3NicuAScmJyMmBzYXFhcWFxYGBwYHBiYnJicuATc2Nz4BFyIHBgcGFQYWFxYXFjY3PgInJicuAQcyFxYXFgcUBgcGBwYmJyYnJjY3Njc2AfBsZGE/QgsMNzw9U0+3VliETwYGHhx1Tk9WAxsFY1tYODoGCEFAQlRRsUxOMDMgFhY6OqdNW1JQMTICTEJFUkybPkBEBB8gPDKANVFJRykqA0k9QEpHjDM2FBUUJyg9TQMiATY1Wl1rXLZJSigoCxwdd6RZW1NShigpBgIxATMyVFdjWKtAQRscEyosR0ezVllCRk4uMjBQU1tTmzU3Dg8nMDGMoklMMiwvMS0rSEpRSocsLQcJMTM1RUKQOz4hLQAAAAAGAAD/lQO+AyQAGwAxAEkAYABoAHEAAAEmBwYHBgcGFhcWFx4BNz4CNzYnLgEnJicjJgc2FxYXFhcWBgcOASYnLgI3Njc+ARcmBwYHBgcGFxYXFhcWNjc+AicmJy4BBzYXFhcWBxYGBwYHBiYnJicmNjc2NzYXFBUjFTMVNycWHwEHNSM1MwHzbGVhQUILDDc8PVNPt1ZYhE8GBh4cd09QVwMVEGNdWTo8CAk9PkCmtE1QZCAVFzk4ol9bVVEyNQEEJCRBRFJNnj9BRQQeIDwwfEJQSkcrLAEBSD5AS0eMMzYUFRQnJz1Kh+LipYkRIjNm398DIgI2NVpdbFy2SUooKAscHXekWVtTU4YoKQUCMQMxMFNWY1isQEM7ECssj7NVWUJETisDMC9PUltTUE42OBAQJTAyjKJJTDIrMDACLCpGSlBLiywvBwkxMzVFQpA7PSIrdDIyvmTDehQoPXk2hQAAAAMAAP+WA78DIgAbADIAPQAAASIHBgcGBwYWFxYXHgE3Njc+ATc2Jy4BJyYnJgc2FxYXFhcWBgcGBwYmJy4BNjc2Nz4BFwYCBzY3NjcXJicB62xjYD9ACgs5PT5UTrRTV0BDUAgHHRt5UVNZDQ9PSEcrLAEDQDk8SEaQNzkyECYoQCNSKRhNFxcqJxR9GTEDIjc2W11rXLdHSicmCxwdOzmhWFtTVYspKgQBjgIpKEVHT0mJLjEMDikwMYeXPkEjFRZuQv7zQRcwKxWHTp4AAAAEAAD/agKiA1IAAwAUAB4AKQAAJSEVIQERMzI2PQE0Jz4BPQE0JyYjBzMyFh0BFAYrAQczMhcWHQEUBisBAqL+pAFc/qSwVVZRIyAmKFQ5OR8aICIwBTUlEBIcHUMpvwPo/TZUUjp7IhJGNxxQKCpkJCgmKSNuEhQwPiYhAAAAAAQAAP+aA7gDIgAQABQAagBvAAABIgcBBhQXARYyNwE2NCcBJgcJAiUxDwMVLwIPBB8CIw8DFR8DMw8CHwQ/AhUfAzM/AzUfAj8ELwIzPwI1LwIjPwIvBA8CNS8CBzA5ATAB9A8L/mEKCgGfCx8KAZ8LC/5hCw8Bhv56/noBfQQEAgFjAwQEBA0CAQECY4wEAwIBAQIDBIxjAgEBAg0EBAQDYwECBAQSBAQCAWMDBAQEDQIBAQJjjAQDAwMDBIxjAgEBAg0EBAQDYwECBIsDIgv+YQofC/5hCgoBnwsfCgGfCz7+ev56AYbGAQIDBIxjAgEBAg0EBAQDYwECBAQSBAQCAWMDBAQEDQIBAQJjjAQDAgEBAgMEjGMCAQECDQQEBARiAQIEGgQCAWMDBAQEDQIBAQJjjAQDAzAAAAAEAAD/mgO4AyIAEAAUABoAHwAAASIHAQYUFwEWMjcBNjQnASYHCQIlDwEXITcnFwcjJwH0Dwv+YQoKAZ8LHwoBnwsL/mELDwGG/nr+egGGB9BSAQpS17tI5kgDIgv+YQofC/5hCgoBnwsfCgGfCz7+ev56AYbgBZf9/X6H3NwAAAIAAP+aA7gDIgAPABMAABMGFBcBFjI3ATY0JwEmIgcJAzsKCgGfCx8KAZ8LC/5hCh8L/pQBhgGG/noBeAofC/5hCgoBnwsfCgGfCwv+RwGG/nr+egAAAAAEAAD/mgO4AyIAEAAUACEALgAAASIHAQYUFwEWMjcBNjQnASYHCQIlIg4BFB4BMj4BNC4BBzIeARQOASIuATQ+AQH0Dwv+YQoKAZ8LHwoBnwsL/mELDwGG/nr+egGGOWE4OGFyYTg4YTkzVjIyVmZWMjJWAyIL/mEKHwv+YQoKAZ8LHwoBnws+/nr+egGG0jhhcmE4OGFyYTgXMlZmVjIyVmZWMgAAAAADAAD/lgO/AyIAGwAyAEoAAAEiBwYHBgcGFhcWFx4BNzY3PgE3NicuAScmJyYHNhcWFxYXFgYHBgcGJicuATY3Njc+ARcmBw4BBwYXFhceATc2NzY3PgEnJicuAQHrbGNgP0AKCzk9PlROtFNXQENQCAcdG3lRU1kND09IRyssAQNAOTxIRpA3OTIQJihAI1IpNjIvQgkLEg4oJWk1OCwuGhkEFhYsHksDIjc2W11rXLdHSicmCxwdOzmhWFtTVYspKgQBjgIpKEVHT0mJLjEMDikwMYeXPkEjFRZRAhoZWTQ4MzQoJSUGByAdMi9vMDQgGBoAAwAA/5YDvwMiABsAMgA2AAABIgcGBwYHBhYXFhceATc2Nz4BNzYnLgEnJicmBzYXFhcWFxYGBwYHBiYnLgE2NzY3PgEXBgchAetsY2A/QAoLOT0+VE60U1dAQ1AIBx0beVFTWQ0PT0hHKywBA0A5PEhGkDc5MhAmKEAjUilgYAGAAyI3Nltda1y3R0onJgscHTs5oVhbU1WLKSoEAY4CKShFR09JiS4xDA4pMDGHlz5BIxUWYqytAAIAAP+SA8EDIwAaADEAAAEiBwYHBgcGHgEXHgE3Njc+ATc2Jy4BJyYnJgcyFxYXFhcWBgcOASYnJicuATc2Nz4BAfBkXltBQhUUIGBJR69YWklKaRYXDg1iSkxZNCdKREErLQcJMDM0hpA8PiImBx8gPCpmAyIvLVBRYlazmzIzKA4PLy2RVVhWWp42OBILjiUkPj9JRoszNSsUJyg9QJpFSCwgIQAAAAMAAP+WA78DIgAaAC8AOQAAASIHBgcGBwYWFxYXHgE3Njc+ATc2Jy4BJyYnBzYXFhcWFxYGBw4BJicuATY3Njc2FwYPARchNj8BJgHrbGNgP0AKCzk8PlRPtFNXQENQCAcdG3lRU1kcT0hHKywBA0A6O46QNzkyDycoQEhWJEdsUgEKFysQRwMiODZaXmtctkdKJyYLHBw7OqFYW1NViykqBI0CKShFR09JiS8wGikwMYeWP0EjK1YaNE79RIgxNAAAAAAEAAD/lgO/AyIAGwAyADcAPQAAASIHBgcGBwYWFxYXHgE3Njc+ATc2Jy4BJyYnJgc2FxYXFhcWBgcGBwYmJy4BNjc2Nz4BBxYXNjcFFBUhEQcB62xjYD9ACgs5PT5UTrRTV0BDUAgHHRt5UVNZDQ9PSEcrLAEDQDk8SEaQNzkyECYoQCNSflZVOHL+hQGayQMiNzZbXWtct0dKJyYLHB07OaFYW1NViykqBAGOAikoRUdPSYkuMQwOKTAxh5c+QSMVFppFRS5cIIyMARKjAAAAAwAA/5YDvwMiABsAMgA6AAABIgcGBwYHBhYXFhceATc2Nz4BNzYnLgEnJicmBzYXFhcWFxYGBwYHBiYnLgE2NzY3PgEXFBUjFTMVNwHrbGNgP0AKCzk9PlROtFNXQENQCAcdG3lRU1kND09IRyssAQNAOTxIRpA3OTIQJihAI1Jb4uKlAyI3Nltda1y3R0onJgscHTs5oVhbU1WLKSoEAY4CKShFR09JiS4xDA4pMDGHlz5BIxUWczIyvmTDAAAABwAA/5UDvgMkABsAMQBJAGAAZQBvAHUAAAEmBwYHBgcGFhcWFx4BNz4CNzYnLgEnJicjJgc2FxYXFhcWBgcOASYnLgI3Njc+ARcmBwYHBgcGFxYXFhcWNjc+AicmJy4BBzYXFhcWBxYGBwYHBiYnJicmNjc2NzYHFBUhEQUyMwYHBgcGByY3FBUhNRcB82xlYUFCCww3PD1TT7dWWIRPBgYeHHdPUFcDFRBjXVk6PAgJPT5AprRNUGQgFRc5OKJfW1VRMjUBBCQkQURSTZ4/QUUEHiA8MHxCUEpHKywBAUg+QEtHjDM2FBUUJyc9SngBmv7BcnIPIBgNFRBA4/7InAMiAjY1Wl1sXLZJSigoCxwdd6RZW1NThigpBQIxAzEwU1ZjWKxAQzsQKyyPs1VZQkROKwMwL09SW1NQTjY4EBAlMDKMoklMMiswMAIsKkZKUEuLLC8HCTEzNUVCkDs9IiubnJwBODEJHBYJDwI0BV1dun0ABQAA/5UDvgMkABsAMQBJAGAAaQAAASYHBgcGBwYWFxYXHgE3PgI3NicuAScmJyMmBzYXFhcWFxYGBw4BJicuAjc2Nz4BFyYHBgcGBwYXFhcWFxY2Nz4CJyYnLgEHNhcWFxYHFgYHBgcGJicmJyY2NzY3NhcGDwEXNRcRBwHzbGVhQUILDDc8PVNPt1ZYhE8GBh4cd09QVwMVEGNdWTo8CAk9PkCmtE1QZCAVFzk4ol9bVVEyNQEEJCRBRFJNnj9BRQQeIDwwfEJQSkcrLAEBSD5AS0eMMzYUFRQnJz1KRTZsL9HKygMiAjY1Wl1sXLZJSigoCxwdd6RZW1NThigpBQIxAzEwU1ZjWKxAQzsQKyyPs1VZQkROKwMwL09SW1NQTjY4EBAlMDKMoklMMiswMAIsKkZKUEuLLC8HCTEzNUVCkDs9IiujJk0hlI+PASiPAAAAAAQAAP+WA70DIwAWADEAOwBCAAABJg4DFhceAjc+ATc2NzYnLgEnJgc2FxYXFhcWBgcGBwYHBicmJyYnJicmNz4CFwYPARchNj8BJicWFwcjJzYB9FimgUQBQD07obBRVIYnKQMGHh52Tl5rVlJQOjsXFxcrLERGWlhWWENDJyYDAyMecpRPJEdsUgEKFysQR5B8P0jmSD8DIgFGfqGzpj4/SQkeHXpRU1hXVVOHJi8xASYlQ0RUT6dISi80EA8WFjk2UE9XWk5JcD60GjRO/USIMTRKWi3c3C0AAAAGAAAAAANFApUAKQBPAFMAVwBbAF8AAAEPAQYHBgcUFxYXMRYXFhcWBgcGDwEhNzM+AScmJyYvASYnJjU0NzY/AQUhBgcGFQYXFhcxFhcWFxYHBgcGByE2NzYnJicmLwEmJyY3NDc2FxUzNQcVMzUHFTM1BxUzNQF8BAJUKSYCGxAoJBAXAwEJDBtISgHKBQFNRQUEFw8jDSIQFB0kTUn+RAEaJxUnARsQKCQQFwMBBAQNGUX+5R8QIwUEGA8kDCIPFQEcIQrc5eV93b7lApUCATIwLi4oKBgpJRUeFQ0ZECMrKwMtVy0hIhQkDiIXHhkdISouKy4dGi4uKCgYKSUVHhUNDA4PISkXFiwsIiIUJQwjFh4ZHSEnIhUVcxYWdBUVcxYWAAAAAAQAAP/TA9QC6QATACcAawDIAAATIg4BFREUHgEzITI+ATURNC4BIwUhMh4BFREUDgEjISIuATURND4BFyIHOQEGDwEGBzkBBgcGHQExFBcwMR4BMxY3OQEyNjU0JzMyNjcwMTYnPgE3NjQmKwE2NTkBNCcuASsBNzY3PgEnJicHMDEyFzkBFgYHOQEGDwEGFzEWHwEzFjczMhcWFAcGIyInFRYzMhYVMRQHDgErARUzMhYHOQEOASsBFTMxMhYVOQEUBwYHITEmJyYnOQEmNTE0NzY3Nj8BNj8BNjPXNVk1NVk1Ajo1WjQ0WjX9xgI6JT4kJD4l/cYlPiQkPqoKCBdpGR0NBQEBBQs0KYmJEREDChARBAMHCA4ECBMTDgUKBBAIxgkUBQkCBwoMAwQDAwIEAhUZBAMCBwICMWRECQIFBQMIZjNDhQcJBAMEBcirBwsDAQcHq4IGBgMDBv7uIhMWBwUBAQQLFkFIDwIEAQLpNFk1/m41WTQ0WTUBkjVZNDwkPSX+biU9JCQ9JQGSJT0kKAYPSBEVJw8UDBcHHBgrKgICFhAKCREPFA4BCQYMJRwMDBILBgcJFAYMHQsOARYFBQ8GAxUZBgcHAQEBAQUFFwUGARYBDQsLCAMDFhAOBwcWBwwHBgMBAhATIBUaGQwUDiAQLDIKAQEAAAACAAAAAAOGAmAAAgAHAAATBSUFESERBWMBkQGR/N8DIv5uAmDj41T+QAHA4AAAAAAFAAAAAAMsAncAJgBOAIYAjwCYAAABFBUGBycHFwYHIxUzFhc1BzUzNzY/ASc3Fzc2PwE1MxUzJicmJzUHFQYHJwcXBgcjFTcWFwcXNxYXFTM1NjcXNyc2NzM1IyYnNycHJic3BzMHFxYfATcXBxcWHwEzFQ8BBg8BFwcnBwYPARUjNScmLwEHJzcnJi8BBzUzNzY/ASc3Fzc2PwEXIgYUFjI2NCYHMhYUBiImNDYBnRYWKEooDAY4OAgUMTEDBhEIIhkiDBogDiReBhYRDgYbEShKJwsGOTkHCyhLKRMZaRkTKUopDAY4OAgLJ0snFhYBRyQBDx4cCyIZIQgSBgMxMQMGEQgkGiMMGx8OIw8fGwskGiQIEgYDMjIDBhEIIxkjDBgiDhIgLy9ALy8gExkZJRoaAnccHAYMJ0ooExhqGR5bASQOHxoMIxkiCBAIAzAwBwkIBDdlNwgLKEsoFBdqARUXKEooDAY5OgYMKEsoFhZpGRInSicMBjcjMAMGEQgiGSIMGx4OIwEOHxoMIxokCBIGAzMyAwYSCCQZIwwbHw4BJA4eHAwiGSIIEAgDUC9BLi5BLyMaJRkZJRoAAAAABQAA/9MD1ALpABMAJwArAC4AMwAAEyIOARURFB4BMyEyPgE1ETQuASMFITIeARURFA4BIyEiLgE1ETQ+AQcRIREFIQcnFzcRIdc1WTU1WTUCOjVaNDRaNf3GAjolPiQkPiX9xiU+JCQ+AQIO/igBotHw8PH+HwLpNFk1/m41WTQ0WTUBkjVZNDwkPSX+biU9JCQ9JQGSJT0kU/6lAVsXiYOenv7ZAAAAAwAAAAADEQKdACAAWAB7AAABIgcOARU5ARQXFhcGBwYPARUhNScmJyYnNjU5ATQmJyYHMhcWFxYfARYXFhcxNj8BNjcWFRQHBgcfAhYVBg8BDgEiJi8BJic0PwMmJy4BJyY1ND8BNgcwFQYXFhcWFxYyNzY3Njc2NzYnNRYXFSM1IxUhNSMVIzU2AfArJCInEAsQOyg1GgICQgIZNCg6KCciJGgJBRoQDAkECAYKEBkOAgQCCgkMHgQcBAEBAQEQNzo3EAEBAQEEGwMEAwcQBg0PBgw0AwICCxYiIEggIhYHAwIBAwRWIFMj/vAjUyACnBUTRCYiJRoRFBojMQTn5wQwIxoUKUomRBMVUwEBBQQGAgYCAwEBCAICARQaJxYbEiAJFAgPBAQCEBISEAIEBA8IFAkiBAIFFw0fGh8ZAgXhAQwSFAsWDAsLDBYGCQYKDREBJjm7hISEhLs5AAAAAAIAAP+WA70DIgAXADMAAAEiDgMWFx4CNz4BNzY3NicuAScmIxcyFxYXFhcWBgcGBwYHBicmJyYnJicmNz4CFwHwV6V/RAFAPTuhsFFUhicpAwYeHnZOXmsIVVBOOToWFxcrLERGWlhWWENDJyYDAyMecpRPAyJGfaGzpj4/SQkeHXpRU1hXVVOHJi8xJyZCRFJPp0hKLzQQDxYWOTZQT1daTklwPgEAAAUAAP+VA74DJAAbADEASQBgAGsAAAEmBwYHBgcGFhcWFx4BNz4CNzYnLgEnJicjJgc2FxYXFhcWBgcOASYnLgI3Njc+ARcmBwYHBgcGFxYXFhcWNjc+AicmJy4BBzYXFhcWBxYGBwYHBiYnJicmNjc2NzYXBgIHNjc2NxcmJwHzbGVhQUILDDc8PVNPt1ZYhE8GBh4cd09QVwMVEGNdWTo8CAk9PkCmtE1QZCAVFzk4ol9bVVEyNQEEJCRBRFJNnj9BRQQeIDwwfEJQSkcrLAEBSD5AS0eMMzYUFRQnJz1KVRhNFxcqJxR9GTEDIgI2NVpdbFy2SUooKAscHXekWVtTU4YoKQUCMQMxMFNWY1isQEM7ECssj7NVWUJETisDMC9PUltTUE42OBAQJTAyjKJJTDIrMDACLCpGSlBLiywvBwkxMzVFQpA7PSIrb0L+80EXMCsVh06eAAAAAAYAAP+VA74DJAAbADEASQBgAGoAcQAAASYHBgcGBwYWFxYXHgE3PgI3NicuAScmJyMmBzYXFhcWFxYGBw4BJicuAjc2Nz4BFyYHBgcGBwYXFhcWFxY2Nz4CJyYnLgEHNhcWFxYHFgYHBgcGJicmJyY2NzY3NhcGDwEXITY/ASYnFhcHIyc2AfNsZWFBQgsMNzw9U0+3VliETwYGHhx3T1BXAxUQY11ZOjwICT0+QKa0TVBkIBUXOTiiX1tVUTI1AQQkJEFEUk2eP0FFBB4gPDB8QlBKRyssAQFIPkBLR4wzNhQVFCcnPUpVJEdsUgEKFysQR5B8P0jmSD8DIgI2NVpdbFy2SUooKAscHXekWVtTU4YoKQUCMQMxMFNWY1isQEM7ECssj7NVWUJETisDMC9PUltTUE42OBAQJTAyjKJJTDIrMDACLCpGSlBLiywvBwkxMzVFQpA7PSIrVxo0Tv1EiDE0Slot3NwtAAAQAAD/oAO7AyIACgAYACYANQBHAFgAaQB3AIQAkwChALEAvwDSAOEA9gAAAQYCBzY3NjcXJi8BFh8BJwYHBgcGNzY3NhMGBw4BFjc2FxY2JicmFyYGFhcWFxY+AScuAScmBQYHBgcGBwYHBhYyNzY3Ni4BMyIHIgcOAR4BNzYXFjY0JyYHBgcGDwEGBwYeATc2NzY0JgUmBhYXFhcWMjYnJicmBSYHBhceATYnJjc2JgUmBhcWBwYeATc+AScuAQUmBhcWFxY+AScmNy4BBSYHBgcGBwYeATc+ATcuAQUiBhYXFhcWNiYnJicmBQ4BBwYHBg8BDgEWNz4BNz4BJgUiBhYXHgEXFjYmJyYnJgUGBwYHBgcGBwYHDgEWNz4BNzYuAQH0GE0XFyonFH0ZMTIMFiJECBkSBgoBCxQWDEovDAETDF5mDA4FDDHKDA8DC0wtBxkPBBhJLgT99QsKBgsIBUEhBBEYBylRBgEN/wQKEgcNCQkUCU5HDBILQuoNDgkQCj0ZBREZBilQCA4BfwwOAgo0GQcZEAQaRAT9wxkFCxsGGBICGhABDgLpDw0DATAEEBgHHRsEAg78pA8OAwIxBxkPBS8CAQ0C7A8HBQENMgYPGQgdJgYBDf2oCwwCCD9UDBEBC083BgGfDCsLExkPHhEMBQ8ML1snCAEN/j0LDQMKK2M0DA4FDGRPBgIYCQkFCg4IHiYfKgsCEQ02ZSkHAg0CJkL+80EXMCsVh06eUSRJbkoHHRQHCwYhQ0wBdAIQBhkRBBkWARMZBAxTARMYBThbCgETDDJYIAIKAQcFCwgEPlAMEgtdQAcTDgECBBURCAMGHwQRGQYfJwIIBQwHMDAMEwELQC4HEw87ARIXBjVUCxMMXUEDqQIrVk0LAxEMUFQKDi0BGg5lXgwSAQo2eT4IChABGg5rWgoCEwxbZggLOAENChE/QwwUAwsmVy0JDqARFgZJHQMSGAYeQgZNAxUDBwQDAwIEGBQBARkYBxUPJxMWBSIvCgEUGAQVQgMDAQUDCQsDFA8MCgYZEgMLMSMHEw4AAAAFAAD/lQO+AyQAGwAxAEkAYABoAAABJgcGBwYHBhYXFhceATc+Ajc2Jy4BJyYnIyYHNhcWFxYXFgYHDgEmJy4CNzY3PgEXJgcGBwYHBhcWFxYXFjY3PgInJicuAQc2FxYXFgcWBgcGBwYmJyYnJjY3Njc2FxQVIxUzFTcB82xlYUFCCww3PD1TT7dWWIRPBgYeHHdPUFcDFRBjXVk6PAgJPT5AprRNUGQgFRc5OKJfW1VRMjUBBCQkQURSTZ4/QUUEHiA8MHxCUEpHKywBAUg+QEtHjDM2FBUUJyc9Sofi4qUDIgI2NVpdbFy2SUooKAscHXekWVtTU4YoKQUCMQMxMFNWY1isQEM7ECssj7NVWUJETisDMC9PUltTUE42OBAQJTAyjKJJTDIrMDACLCpGSlBLiywvBwkxMzVFQpA7PSIrdDIyvmTDAAAAAA0AAP+iA7gDIAAEAAgADAAQABQAGABTALwBRQGiAekCMwKfAAABFREhEQUzESMTFTM1BxUzNQcVMzUHFTM1AysBByMHIwcVDwUVHwUzPwMzNzM3MzczFzMXMxczHwEzPwM1LwQjNSMnIycXDwYVHwozHwYVHwIzHw0zPwQ1LwE1JyMvBiMvATUjLwsjLwEjLwcjNSc1LwQjBQ8BIw8EFQ8EFQcjBxUPAyMVBxUHIxUPBBUHIw8BFQ8CFQ8CIw8DFQ8DFQcVDwoVBxUfBD8EMz8ENT8mNS8DAQ8FFRcVBxUHFQcVBxUHFQcVBxUHFQcVBxUHFQ8HFR8EPwUzPwEzPwE1PwEzNzM3NTc1MzU3NTc1NzU3NTc1NzU3NTc1JzUvBAUPBRUXFRcVFzMfBTMfARUXFRcVFxUzFxUXFR8EPwQ1Lw01JzUnNSc1LwMBDxcfBD8EMz8DMzczNzM3MzczNzU3Mzc1NzM/AjM3MzU/AzUvBAUPBRUfAxUfATMfAjMfAhUfATMfARUfATMXMxczFzMfAjMXMxcVMx8HMxcVFzMVMxczPwQ1LwQjJyMnIycjLwojLwQjLwUjAV0BLv7q/v4aysrKysrKymEHDQYNDQcnDQ4EAwMEAQIDAwkEBQYMBhcGCwYGBgYpBgYGBgYFHwQFDAMDAgIGAwonBgcGDQfpBQQEAwMBAgICBgICAwYFBAMIAQ4CCAIGAgQBAgIBAgMCAwUEAwQDCgUGAwkECQQEAwQCBAYBBAMCAwIDAgECCAEFAwQCBAICBQIHAgIBAgkBAgIDBwMCAwIBAgMCBgIKBf3zBQcBAgMCAwIDAgMCAwQBAgIDAgQBBAQBAgICAgICAQICAgQCAgQBAQECAwICCgECAgIBBAECAQIBAgECAgEEBAMOBAgEAwEBAgMBAgICAQIBAgECAQICAwIBAggCAQICAgEEAgQBBgIKAgYDCgMGAwUDAgEEBggFArIEBQMGAgEBAQEBAQEBAgEDBQMFAwQDAgYDAQIDBgQJCQQEAwIBAQMCAQIDAwIBBQEMAwECAQEBAQEBAQEEAwQECPymBAUDBAQBAQMCAQMCAQQBCgEEAwIDAgECAwECBAcJCQQEAwQCBAIDAgcEAQYBAgECCAEBAQEFCAgCsQQECQQFCgQPBQULDwYFCwUhBgsQCAQFAgIEAwQNCQYGFAUBEgYHBQEFAQUBBQEcAQsFAQUFAQULDwEEAQMDAgEEAwQECP3eBAQEAwICAQIDBA0CAQIDAgECAwMLAgEIAwMFAQIBAgECAQIDAwECAQIBAgQDBgMNAx0CAw0EAw0JBQgFAgEEAwQEAwQIAxQCEQMJBRECBgUDBQUFAgEPAgUCAgEEAwQRBAQFAhwM/pABfBj+tAEnGRlKGBhVGBhNGBgCLQECBgECBAIDBAgJBQQEAwQBAgICBAIBAQEBAQUBBgQDCQUICAIEBgEBAU8BAQMDBAQEBQkEBgECAQYDBAIIDgMIAwYDBAECAgMEAwQDBwcDCAMSCggCBAICAwMICgcHAQwIBAQEBAQEBAsBBwMGAgYCAwUDBwMCAwkDAgIHAgICAgEBAQICBAIFDAEEAgICAgIBAgICAgIBBAIBAgIDBAEEAQQBAgMCAwIBAgMCAQIGAgECBgMDAgUCAQIPAwIBAgECAwYDAwMDAwMDAwEECQUIAwMDAQQDAwQGBQMCBQECAwIDAgMCAwMCBQIDAgwCAwIDAgMEAwQDBgMKAwYCCgEGAgUEBAQJCQYEAf7GAQICCAQEBAQzAwkDBgIGAwMDAwMDBgIDAwkCDgMIAQ0FCwUGCgcEBQkEBgIC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CAQDAwMBBgMDAgMCAwIDAgMHFgEEAQQBBgMCAgQEAwQFCQQEBAMEAQICAgECAQIBAgICAQEBCAEBAQICBAECBwMCAQIDAgECBgMJAwMDBP3DBQQIAwMCAgECAQEBAQEBAgEIAgEBAQMDBAQEBQkEAwMEAQIFAwIBAQEBAQECAQECAwMJAu4FBAgGAgEBAQEBAggCAQICBAEWAQEEAwwFCQQGBQQDCgIIAQIBAgICBwEBAQEBBAMECPynBQgIBAIBAQECBgICAgEEAQYCCgMEAQMGBAUJBAQEAwQCARIBAgECAQICAgcCAQEBAQIHAwUC8AQFBAQDBAICBwIDBQIEAQIDDAoDBAIEAQEEAwQECQgFBwMPAgEGBQgBAQEGAQIBAgECAgIJAQICAwQI/a0ECAQDAgICBQYMBgEMAQYHAwEDAwEHBwMBBwcBAwEDBAgEBAwEBAQECQQEBAUCAQEEBAMVAwQKBxkDCgIDAwMDAwMDAgYDAgMCAwIIAZwFBAQBBwocCwcSBAsEBwQEBwQIBBAIBAMCAgEEBAMEBQwFDQQFBAUIBSYECQQEBAQEBAEEAwEDAQMEBAgECQQCAgECBgMEBf5EBAUEBgICAQIDAwUEAQQNCQENAQQFBQQBBAUFCgUZAQQBFAYFBQULCgkFBAQDBAECAwMEBxMFBAUFBAUFBCUeCAQECQwDDQISBQQFAwEUBAQJCBYsBQUEBQUFBAUbBAYEAQECAwMEBAQJAwYFCwUFBQYUAR4FCgUFBAEEAQQFHA0EAQQFAwIBAgIDBAgCHAz+kAF8GP60AScZGUoYGFUYGE0YGAIvAQEBAQcCAgQDBAQECQQFBgMCCQIBAQEBAQECBQECAwMIBAUECQMDAwcBAQEBUwECAgQHCQkKAQkHBgcDBAMEAwQHCAcUBA0EBCADAwICAgIDAwgJBQYFBQQBBAEEBRwNBAEIBAEEBAQIAQMBAwEDBAgDAQMEAwEGBAcIAQQEAQMEAwEDDAMBAwEDAQgEBAQBBAQEAQQNFwUFBAEEAQkFCgUMBAUEBAMEAQIFBwweCAQJBBQHCAcEAwQDBAMHBgQDBwUEBAkEBAMEAQICBQwFCQQDBgIBAQEBAQICBwICAwICAgECAwMIBQkIAwUCAQIBAgECAgIJAgEBAScCAwUCBAECDwMCAQIJDAkBCQQDAwEDAwEDCwcHCQkE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AQICAQQFAwECAgIHBAYDBAMCBQcBAQIDBAEFAQICEgEBAQEBAQEIAQYBVgEFAwIDAgEBAQMCBwMBAQMCAQgGAQIEAgECAgcGBAQDAwIZBgQEAQIBCgECAwIDAQEBAQIDBQIFAgQCAQEDAQQCAgEBAQEBAQMEBAECAQMFBQEBAQECEQIDAQECAQEMAQEcAg4CAwgGAQIBDgoCBAIBBQECCAkBBAICAQQDAQYDCQ4CAQIEAgYBCgMHBAMBAQQBAgIEBwQNAQgBBgQQBAICBAQHBQEBBQQJAwICAgMFAQECAQEECQQGAQEBAQIEAgICAQQGAwEEHSEBAwUBAQMbFwoB0wIBAwEBCgIBAQEHBwgBAQURCwcGAQEBAQMCAgQFBwkCBwQDBwgMCgcBAgEDBQICBAMCAQEDBgYFBAoFDw4BAQIEAQEGAQIECAMCAQMBAgYBAgIBAQEBCwYFAQIEBgYCAgUBDQkGAQEBCgMCBQkBAgUEBAYDAQMFAgMBAQIEAQQGBQcHAQEBAQMIAwcBAwUNAwoDAgQBAQEDAgECAQYLAwEBAgECCgQD6gMJCgICAgECAgQCCAYDCAMCBQEDBQMBAwICAQMBAQQDAQECAwIBBAkEAQEHAwMDAgEDAQECAQIBBQEBChEEAwIBBAIFBAcDAQgFAwYEBwIFBQMCAQECBQsFEgUCDwEGBwEBAwMCAwEEBAEBAQMFBwIEAwICAQIBAQEDAQMCAQIBAwMBBAEEAQEBAgIDBQQCBAEGBwoJCwcBAQcEBAUOAgUBAwQFBAEDDAQFAQICAgECAQECBQIEAgUMBAEBAQQBAQEEAwEBAQIJBwUBBgEBAQMJCwEFBwYEAwEBAQMBBwYJCwMHAQEDAwIBAgECBAcKAwEBAgQBBAEEAQQCAgEBAwQBBAMCBAMBEQIDBAsOBAMBAgEBAwEBBAECAgEBAQEBAwMDAQMDAgYDAwkLAwIBAgEDAwYIAQMCBAECAgIEAQMJCQMFBAoBAwQBAQECBAQBBAMBAgMBBwIDBAEHAgECBQMEBQQBBwMDAwEFAQICBQYHBwIBAgIBAQMBBA0EBxAMBwQBAgMBAwIJAQIBAgEFAQQCAgEBAgIBAQIFBAgPAQYJAQIBAQMBAQcLAwEIAgIBAwIDAQIGAwMEAQECAwIIBA8HAgQBAgcFAwICAgEBAgQFAgMBAgUCBAMBmAYGCAoCBgcKCgcCAQQJBQ8LAwQEBgQFAgEBAgoHCAgBAQEOBwcFAQECAQIDAQEBAgEKAwECBAEBAgIBAQECAQEFCgICAQQBAQEEAwYIAQICAgMHBgMDDQcBBQECAQQBCAQLBwMBAQEBBQYHBAMEAwMBBAIJBAQHBwEBAwENAgIDAQIEBAgFCQUCAgMBAgIDAQULBAIBAQsFBgICAQEGAQIBAQEDBAIEAQIEBwQCAgIBAQMCBA8CJyEBBAEBAQICCBsBAg4BAQEDAgECAQQBAQEDAQE/AQEBAgMHAwQBAQMFAgECAQEBAwQEAwECBAIFAQMCAQkBBQMBAwEBAQEBAgECAgQIAwYBAQUBAwMOBwcGAwEBAgMFAQIFAgEBAQEDBAMCAQIBCAUDAgEBAQECAgMMBAQMCQEHAgEJAwECAQICAgEDAwEBAQEHCAwBAQQIAgUBAwMCAQICAgQBAgEBAQIBAgICAgMBAgEBBAQBAwEGBAoHAgEBAgUDAgECAgECAwIEBgMDAgIBAgEBAQEFBAECAQIFAwMDBgQEAQEEAQMDCAUJBQEBBwEEAwEEAQEBAgEBAQIHcX4BAUkCATMBAQEGBwEBAgIBAgEBAQEDAgEBAhPOAQGZATALAQG1AZoCAawCARYB7wICFbYcAQEBAQEVIsbJAgIFAgEBAQEBBQEBoboBBQUBBAQCBgYIAwcFAQQhASUlBQUBBAEHAwchARUpYFMCBAEGAwMGBgEBAQIEBQIBAQICAwUBAgYFAQECAQMBAwEeARMBCAEBDQOlAQEBBQMEAQEBAwMBBAQBAQICAwQHAggCAQICBwUDBQMBAgICAxQBAgQCAQEBBAcDAQEBAwUDAQECAQUBAQMBAgECAwQBAgMIAgECAQQDAQEEAQYEAQgCAwcDAR4BDAUOAQEvAQEBBQMKAgYCBAcCBAQEBhAFBAQKAwMBBQMBAQIDAQEBAgEFAQcDAQQCAgEBAQEBAgQBAgYICgcBAgQDAwEBAwECAgILAQQBAgUEAQMBAQECAQIDAwECAQEEAgMCAwICAQECBgQDBwQBBggEBwgDAgEBBAEBAQICAQEBAwMCBAEICAYGBAYEAggGAQkDAQIEAQUEDAMBAwYIAwYDAgQBBAECAQUECgMBBAEBAgEGAgICAgMCAQECAQEBAgYFAgEEAQIEAQIGAgECAwQCAQEBAQECAwICBAMCAgMCAgoBBQYCAQIBAgEDAgMEBQYCAgEBBgICAQIECQUBCAYCAwIDAQECAgQJAwEEAQEGCgQJEAgCCQMEAgEBOgEBAQIQAgEBTgEEAjYrAQ8BAjIkDQEBKAEBDhAGJTUIAgIDAgEBAgIDAgEBAQQEDhYKAQYDAQECAQIEAwQCAQgIAQEFBQMCBAUBAwECAwEBAQEIBQMBAgcBAgYCAgMBAgIDAQEBAQMECQoBAgQBAQEBAg0BBAgEAwEBBF0FBQcGAwICAgEBBAUHAgkFAQIEAQMBAgIDCAYFAQMEAQEBBwQIAQEBAgEGBQEBAQYEBgIDBQgDAwEBAQMDCxMHAgEBBgUEAQMBAQECAQIDAQUEAQEoAR8UAQMCLwEBBgIBBAECAgEDAQMCAwEHAgIBAQIBAQcEAQIBAQIBAQQBCQUBBgIEAgITBgcBAwUBBAEFAwQJAQEBAgcBAQECAgEBAgIDAQECAwEBAQEBBwQFAQoMAwELAQIBAwMHAgECAQEBFAIBASYBARMCAQEbAQEBUQEIBgICAQEDAwQCBQUBAwIBAQEBAQMDBAEFAQIDAgEBAQQICAQEAQEBAQIIAgECBAMEBQMFAwMBAwICAQECAQEDAgEEAwMEAgQEAQEBAwEPCgEBPAEIAgYCBAEBBRIEBAEEAgMBAQEBAgECAQ0CAQEFAQIEAgkLAgECAQYEAQMIAwQBAQEHBQENCAgCAwECCwIBCAIDAQQPBAEDAQQFBAQDAwIBBAcFAQIBBQUDAQIDCAcCAQEBAwMCAQQCBAEHAgEDAxgNFAUBAQEBAgsBAQUDBQIGAQEUCwUNBQUMChoNAQUFAg4GAwEJBQYRCAwPAwMCAwwUAwQEAwICAgEEAQoCASEBpAICAQEFDR4EIB4OAQUCAgkFFSoBBQMWFwYBAgMEAwIBBQEFARABAwUCAgEBAQoYByAcDwUQCQYBAQcGIx4BCAYcEQkCAQICBAMBAQERBAoCBAMEAgEDAgUDBAEIAxEDAxMCAgYPAgEIBhoFBAQIFQEBAQUCBAMDAgMGAQQBAQQBCwQFAQMDGwQBBQEZDwUJAwQBFAMCBQQBBAIEAQICBQQDFw0DBBEFCgUQDQcCBAEECQQBAQcBDAcBAgEFAQEFBgyTDxIHCAYGDAsHBgQBAgcmBAMCBAIGEBMBBAIEBhQPCAcDFwwBAQMCBgUIDAQLBhEOAQFWDxAHBQcBCBERBAUBAgYBAgIREwkKCAcQCwgFBAECAQQQBAkCAgYFBQsCBwQBDA4JEAcBCAUQQQECF00BAQ8PExYGCwcHCwYGCxQfMTIZAgEDAQEaEg4KEQwIGAMJBAMDAgYOBgMBCQEBEgoIAQEBFwcKDggOExcCJwQJAwgJEQQBFQEcEhEKCgQHBgoHDQQGEAcDAgQGDQ8RBg8GAyHXAQQIAwEHDgICAQMPPwsDATMBAwQFCQYEAQIJAQ8lBgEBBQEECQcFBQIVBAIBAwEBIVQMAgIEBuYBEQsSAgEFAQUGAwgFBgMFBAcDBxQIDgIXFxYVCAEFBgwZEQgQBAkFBAYBAwEFAwUCEA8NCRUHBwkMBwkFDREQAwMGAgocJg4QCwgDAQEBFy0OEjsRBQYBBQQBAwEBAgUFAREJDgEKCRYQGgYJAQMHCQ0KGgwMCQYCAwUJBAIIAQgNDQUKAgYIBhMHCg4CDgcPEgMEBQMEAwwNERQYHg4IAxcVGg4TAgESChIGBQYECQUy2QGTBQMCBA8HDgcCBAUCBAIIBwsJEwUEAZoKDwMFCxk3Q1ViQjsiFAsMDgKnDQkWJg0XEBoaERYNJhYJDQ0JEhIUDRkZDhMJERMNnA0MKyAjK/2SKyMgLBgYLCAjKwJuKyMgFRYMDSwMDCgbFhX9khQYGSoLDAwLKhkYFAJuFRYbKAwM4A0JIBkTECAIAwMIIBATGSAJDQ0RDBENHwcDAwcfDREMEQ0NCQMJDQ0JAwkNcw0JIBcTDhMLBhIDBgECAQMBBQgcDg8GBgMCCQ1pLCxiDQkCCQ0NCQIJDVkG/q0GLB0hLi56fC8SLHFuVCAZJgGcASQiLnx7LwYBVwYiERERAR4c/sEscHAqICJIAQIEBgIIAQIBAQIGBgQBAgEEAQQEAwQCBgMDCwMCAQEDAQECAQEBBwUCAgEDAgMCAQMHBQIDAQMBBAQEAgEBAgECAQEDCAEBAgIBAQMBAwEBCAIDAQIiAQEDAQMGBQEHBAEDAgIFAQUEAwICBgQJAgEBAgICAgEDAgMEBgUKCgQDCQUBAQEDBAoDAgIFBwIEAwEEAwIEAQIDAwQFAQUIBQIFAgEHAgkHAgYCBgYBBQIDAwMLCwYBBAsBAhQCBQUCBQQBBggDAgEBAR0BBwoGCAIEBQQUCgMFAwMDAQoGBwYCCQQDAgIDAQECAQIGAQECBAEDAQEBAQMFAgUCBAICAQICAwIBAgYGAgEEBgUEAQQEBQECAQIDBAEFDAcEAwYBAQEBAwYCAQICAwEDAgQBAgIDAwMDAQMBAwEBAQMBBwEDBAEEAgIFAQEBAgMBAQQBAQIBAQEGAgICAQECAgIGDwEEAQIBAQMBAQICAQEBAQEEAQFOgwQDAQQDBQIBAQEGDhUKAQQFAgMJAQQCAQIDAwMLCQEBAgMEAgIBBQEFDQMBAQEFBAIBAQIEAQQCAwECBAMDAQIEAQEEBwcBAgECBwMBEQoGAwEBAQICAQIIAwQHBAQIAQQFDAEBBAEGAwEBAgEBAQoFAQMFAQIGAgMGARILBQYBAQECAQECAwQCBAEEAwMCCAICBAICBAIBBAMBAQEEAgECAgEEAgkBAQQKAQExAwIFAgsDBAUBCwMKAwEIAgIBAgMJAQMCAgQCAwEBAgITAwQEBgMCAgMBAQIBAQUBAQYBAQEDAgEDBgQCAwIDAQMECgECBQgCAQUEBAUBAgIBAQEBAQIBAQEDBwEBAQECAQQFAT4BAgQFAQEDAgIDAQUEAwkCAwEBBwgGAgYDAQMCBQMIBAcEAgIBAgEbAwEHAwYCCgoBAgIBAgIGBgICAQIFAwEBAgQEAwQCAQEBAQEFCAEBAwIEAQIBAgUGAw8EAQUBAgIMETUCBwMGCwIBBQcSBAEBAQIEAgIEAwYCBAEFBwYCBQ8KAwIBAQQICwYCBQEBAgEDCgEBBAIIFQgCBA4PBgYCBQIGAQcEBgEBAQEEBwMCAwEBAgMHBgYBAwMBAQEBAgEBAQICAgIBAwMCAgwCAQYCAQEBAwYBMQELAZ0DBAUCAgEBAQUBAQUCAQQDCggCAQYBAQEDAQIBAwkBBgEBAQQLCgIBAQEBAgMCAQECAwIHCQIQBwsDAQMCAwECAwICCAwLBggEAggBBwQEBAIBAQkIAwMCDAcECgUHBQECAgIIAQEBAQIGAgECCQIKAQIBBgMBAQMGBQUBAwEDDAUCBwEBAwEIBAsDAQcDAQUDCAUDAQYBAQECAgMEAQMHCQMDAgUIBeMECAICAwEBAQEBAw0IBwoBAQEDCQYCAwEBBgYEAgMDAwYBAQICBgQGAgMDAQMBAwIDBAECAQIBAQMFAwYEAQ0KBggEBgECBQIHCQEKBAUECgEBAQIBAQEECxEUBgEKAgEBAgMDBgUDAQMDAgEHEAQCAgEBAQICAQIDAQMBAQEBBAQBBAUEAQICAwQMBAIGAwgDDRAMBwQFBgoDBg8FCAgBAgEEAgMBkgIDAwQGBAQEAQIFAgEKCAcFAQEFAwYFAQMDBgEBAQMDAwsFAwEBAgEDAwQDCAYDBAEBAQEDAwYDCwQGAgMEAQUDAgIDAwcFAQEBAQQEAwEDAgEBAgECAQIDAgIFBAMGAQQFBwYEAQIBAgMCAgEBAQEBAQECBQMCAgEDAgcDAQMHAgIBBwQGBAEBAgIBAwICBgMDAwEFAgkMBgMGAQQDAgcBBAMEBgYFAQEBAwYDBAUBAgIEAgEDAgICBAUCDgMCAQYCAQQCBwQFAQUCAQECBwMBBAcDBw0EBgEFAwIDAQEBBAEBAwgBAgIDAQIBBQMEAQwECggBBgMBAQEBAQUEAgMBCA0DAQEBAwQCAQICBQYCAgEBAgEFEAIGBA0KBQEDAwEDBgUGAQIJDgcBBAIClwICAQgDBAEKBQEBAgQNAwYCAQQBAQICAQYGBgECAQQJCQkMBAECAQEBAQIBAQECAQkNAQQHAQwGAQEBAgQCAgECBggEAgECDQcCAQECCQEGBwQKBAkFAQEBAgIDCgEFAQEBAwEDBQEEBQIEAQEBAwYFAwYEAQQEAgUOAQIHAgEBAwwBAwIHAQIBAwICAwQJBgECBAEBAgQBAQIDCwcCAgICAQcJBggGAgEDAQECBQUBAQEBAgICAQMDAgEBAQEBAQQCCQsDCAcBBAEBAgIHAwEBAgEEAQECAgIBAgEBAQEBAgMDAwMDBAMDAQEDAQEBAQECAQIBAwkCAgIBAQEBAQoEAQEDAQIBAQEBBgQGAQYDCAQBAwMCAQEBAQ0BBwICAQIDCAMBAQUDAQEBAQIBAQMFAgEEBAUBAwkDAQEDBgQHCgUEBAMCAgUGBQEBAQEBAQMEBgIEAQMBAwUJCgUBBAEFBgEDBAQEAwECBQYBAQIBAQEBAgEBAwIBBQMBAQQBAQMNAwIDCggDAQIBAQECAgMBAgMIBQUBAQUDAwEEAQEBBAECBQYCBAEHAgEBAQUBAQQEBQEDBAgGBAIBBAICBgEDAgICAQEBAQMBDAECAQMDAgEBAQQBAgEBAQECAQEBAQQBAQEFAQEEAgEBBAEDAgMEBAIBAQEGAQcBCAEFAQgEAgICBQMBAQEBAQIBAgEBAhAHAgQCCwkEBgMECgYLKAQDAQUCAwIDAgEDAg0BAQcBBQ4MBQECCQUBAQECCAICBgMBCQQFAQQCAQIGAgEIAQEDAQUBAggTAQEV/AEBBgUHAQQDAQMBAQICAQEICgQCAQICAQIDAQcGAwEFBAEUAQIBAQEEAwEBAQIHCQQCAwQBBQYBAwMBAQIBAwQDBQQBAwEFAgMBAwkGAwEHBAEBAQEMARoBAQkCCAEBAgYNAwINAQYLCwMBBAQBBQEFBAQHCAMGAQEBAgIFAQMEAQIHAgMBBQICAQMBAgECAwICBAIFBAYDAwkFAwEBBAUBBQEDAQQKBgcGAgICAQEDAgEBAQIEBAIGAQEBAwECBQYBAQEDAQEIAQMBBAMBAQEBAQECAQEBBwIDAQIBAQYZAgYBBgkCBgYDAwwICAEDAQMCAgkFBQMBAgECAQMJBAQCAwEECwIBAQIBAQECAQYEBQIBBAEDAgUCBQIBAQQCAgcEBQEBAQMCBAIEAQEDAQQFCQgGBQIFAwMBBgEBAgECAQMBAgQEBQMBAQMDAQMBAQEEAQICBAYBBAMFAwMEAwIFAQMCAgMDBgQDCRoBBwUBAQIIAQMJAQcCAgIBAQYBFAEBAQMBQwEBAQMCCAQHAQEDAQEJFA0HAgMCAQIEAgQBAQQDAQEDCAIBAQEGBgMBAQEBAwECAwEBBQICAwMCAQICAgMBBAIBAwoGAgUBCQICCgkBAQEBAwIBRAkFAQICAQEBAQEDAQkEAgEEAgECAQIIBQMFBgoBAQIBBQoBAQECBwIFBAMBCAUHBQEBBAMEBAEHDgoGAQEBAwMCAQEBAgICAgECBAoDBA0SLQEBAgEBAQMBAQEGAwEFAgQDAQEBAQEBBgMCBAIEAQECBAUDBAEDCgIDAQEBAgIUAwMFAQYFAQICAQEBCQMDBgIFAQEHAgQCAQEBAQEBAQcCAwMDAQEBBgYBAQEPGwEBBAIBAwMDAQEBAQEBFgIBAwMBAQoBJQcIBgEBAgEJAgEBAQcGBAIBAQMEAQECAQEBAgEFBAIFAwMEAgMCAQMHARYEAQcGBAUDCAoCAQEEAgoEAwECAwgDAgEDAQIFAQEBBAEFAgMDAQEEAQEBAQUEAwEYEwQCNAEGCwgEAQMUCgMFBAIGAQMCAQICAgIBAwIBAQUBAQQPCAUCAQEHBAcEAQQCAQEFDQYBAQMCBQwM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AQEIAQMCAQMCAQYFCQwHEAIGCgYGBAICAwEEBAMCBAQBAwIGAgQFAQcHAgMHBAkFBwUDAgIHAQkCAQMMBg0KBgEBAgMCAQEDAwUCBQgDBQEFAgEFAQMGAQMDBAsFAwIHFQgCBgYHBAkGBgUDCAIECQIECA4MCgUMCwMIAQEJAwJXBwUGAgQBBAMEAQIPAwQNAQ4RAQEBBgsDBAUHBggCAQQCAQECAQEBBwQBBwMBAgECAQEBAQIBAQEDAgUEAQEBAgQEBwsEAgIBBgYDAgIDBQIEAgUEBAIGBAEDAQIDBQQDBAIBAwMBAQEFCAEBAwEBAQEFAQECAQEDAQIBAgQCCwcBDQgKBgEBAQECCQICBAECAQEBAQEBAQEGBAUBAgEEAgoSCwoEAQYBBAEBAU0BAwMCAQEBAQEBAQEBCAEBAQEBAUkIAQYCBAQCAgMGAgwbEwQFAgQEAgEIAQUIAggCCQUKBgICAQECAQUBDQIIBQQFAwEDAQEBBQQCBQQIAwQCBwcJAgIDAQIEAgcFAgIJAgYBAQEKCAI0BAcCBAEBAwEBAQMBAQEBAwMCAwEBAwkGBAICAQMBAQEBAQsFAgQEBQMCAgEEAQEBAwMBDAIBAwQBAwQCBQIBAggQCBgKBAUFAgEDAgUBAQEEAQIBAQEBAgYFBQsFAQQEAgICAgIFAiABAhwyAQIEAg4HAwEBAQECAQMDAQMBAR8gG1UDCQEFAQYBAQILBwcCAQUBAQEBBQoDAwECBggDAwEDAgIDAgEDBQMCCQMCGAUEAQQCAwEBAQIDBQEFBQQEAQIDAgwBAQIGBgMBAQUMBAEGBAMEAwEBAQEBBAEBAwEGBwMZAQEBAgMBAQMCAQUaYgECBQQCAQYBAgUIBQIBAgIDAwMCCwEBAQEEAQIFBwcGAQgDAiIFBwICAQQDBgEEBAUCAwEBAQIEBAQCAgQCAQICAQECBwECAQMBAQEBAwQDAwIEAwIDAgEDBAICDwIICwIECggCAQIBARIMAgYDCAIDFgIBBQIFBgUFBRkGAgIDBAEHAQwFCQUEAQEFAQECBwoDDwEJBAYFFwEBBQMBBAUCBgIBAQEBBgYGAQQEAgEEBgUICwcIAQEEAgIEBQIBAQcDCAEEGh4DAQEBASEdDQEBAgEBBQECBQQCAgIBAQQKAwIHAwEGFg0JCAEBAQIDAwIFBggXBgICBAoFDwwCBAIDAgEGBgQHAwEBAQQHBgYHBgMCEQwGAQMEAgEDBAUCAQoFAQIEAQIIAgMBAQULBQMFAQECAwkLBAEDAQIBAQIFBwIKCQEBAgEEBAIGCAQDAwMDAgMBAwEFCQQFBQIDAgEHAgUHBwcGBAECAgIBBQcFAgQCAgEBBQ8GCwUEAQIBAQEBBAICAgEOBQYBAQEBAgECDgMF/tkCDA0CAQMCAgIHCQgCAwECBQMGAwQGBAMBAgICAgUBCAIBAQIFAQEECwIEAQQEAwQFAgECAgIBAQMDAQEBAQIBAgEBDBYGAwMBAwICAwYCCAQBCQUCAwYBBQkDCQUCAQIBAgIGAQ0GFgUCBAULAQoFAgEBAQMBAwMDAQQBAgUCAwQHCAYFAgEBAgECAwICAQIBAwIDAwEBAgEDBQIFAQMBAgICBAsIAQYEAwkFCg4CBwIEBAQECg8CBwECBgcGAgkIAwMDBwMBBAEBAQEBAgMCAgMDAgYOBQIBAQEBBAECAgYDAg4ECQMHAgIBAwwECAIBBQQIBQUEAQEBAQMBBhUPBAgCAgMDAwECAQEBAgEBAgoLAQQCBAEDAQEFAQICAwIGAgIBAgQBAgIDAQECAQUEAQEVAQIFCw0JBgMDAQEDAgEBAgYBAgICAQQDAwUFAwYBAwUECg4BBAEBBAcIAQkBAQQCBgMDAQEEAQEEDAoEAgICAgQLAQQBAQMBAQMEBgIFBAECBggDBgICCAMBAwcCBgIGBAgDBAQEAgIBBAkFCggCBAEBAgIBBwUQBAEIFQ4JBAICBAICAQMIAwMCAQEDBAQBAgMCAgEBAQMIChMBDgQHAwkOBAELAQECAgQCAQMBBAgEAwYCAgUCCQYRCwYBCgIDBAECAgICAQEEAQQHAgMBAQEFBwICtgsKAQEBAQIBCAcMDAcCAgMFCwYBEgwCBQIBCQQHBgICAQEBBwUICQgCAQESCQgCAQEFAwICBgEBBQwEAQEBAgQBAQIBBAIBBAEBAQMOBgECAgECAwQFCQgDAgIEAQEHAwgDDgkBAgECBQEEAwEIDAIBAQEBAQIGBggDAgEFBgEEAgQECgQEAwUIAwIBAgIBBAgDAQIBAQICAgIBCAQIBgoHAgEDAwEBAwIDAQkKBgEBAQIBDgYGAgEBAQEBBgEBAQYIBAEKCQIEAgEBAQIGGggBAQUBAQMCBwMCAwEBAQEBBQQCAQQCBQIDAQYBAQECBAELAQIBAgUFAQEBAQECAwMDAQMCBAoCBQUBAQEDAwIDAQMMBgEDCAMFAQUEAQQBAgEBAgECBAQBAQEBAgQGBwUBAQEGBQQCAQEBAQIDBg4EAQcHCgsBCgMIAgECAQIEAQEDAQIEAgICAwEBAQIHAQEIEgIHBgQCAgECAQQHAgUBAgQCAwYBAwEBAQIDAQEEAwICAQEBAQMEAQMDCAUPCAIBAwIIAwICAQEBAwECAQEBAQQCAgQEAwMBAQUBBAECAQECAQEGAwECAQoEBQMHBgIBAQIBAwQFEwoJAQMFBAMCAQMBAgIBAQEBAgQBAwEEAgEBAQEEBQQCAgICAgQCBAUGBggBAgQBAgUFAw4MAxQYAQEBAQIBAQEBAQYBYwGdAgIBAwMEBQMCAwECAQQCAQICAQEFAQcOAgMBAgMCAgHiARwEpQO0Ab4B0wEDAQICAQEBAgQFBAECAQEGAf7aAQEL/gIBCQEB/uIBEv78AQEJAQEXAQECAQICAwEOAQMBAfMBDvMBAfgCAgoEAgIBAgEBAgICAQUBAR8BAQYFAQMBBgYEDQEECQUEAQEBAgQCAwUBAQECAQEBAgIBAQEEAwgBCgUSAwPUBAkFAQIHCdHBAq8IBAECAwMBBwUFAgIBEwcDAwIaBAIBAQECpAEBA5spAgICBQEGAgUDBQYEBAQEAQMCAgMECAMEAgMCBggDCQYBAwUEBgEBAQEBAwEBBAYBBQNPAQIDBBkEBQM6As8BAQEGCAICAgQBAQEBCAIBAwMDAQICAgoDCwIBAgcKBQYEAQEBAQIEBQIXBQUBAgIBAQUGAwMDBgIDAQQBAgYBAwMCAwUEAgUCBgECAwQEAgEBAgcBCgQBCAIECgMBJQoCAQgBAQcuAgIFBAgHCQIGCQIFBgEBBAgTBwEBAgEGDQUFBwQCAQMEAQEBAQMCBgIIBQEGAwMBAQIDBQEBAQYCCg4KAgEDBQQEAgECAQEDAwMPAwcCAQgGAgMBAQECAQEDBgEDAQEBAQUBBAUCBQEKBQQJBQIEDAwJBQIDAwYBAgICAgEBAQQIBQEJCwkHBgcEAQwIAQQHAgECBgIGBgIOBQkNDwYHBQIEBQUGAwkDAQQCAQEEAwcDAggCAgEBAQECAgMBAgMGBAIBAQEBAQYBBwoDAQEBAwIDAgUBAQECBAECBgIBAgQDAg4BAwEDBAQEAwEBAgICAgMEBQcBAgICAQECAQICBgMBAQECAwIECgYCDgYIAgMCAQICAwYGAgEBAgQCAQIBAQULAQMDCwsGDAECCAMJBAEBAgFMAgEBTgUCDzYBNg8BAQICMjQsARQMOgEBVgMCBQIBAwQCAgECBQUSHA0EBwMDAgECBQEDBQIBBQwEAQECAwwCBgYEAwMDAQICAQkIAwMBCQIJAgIDBAQEAgEBAgEJAQYFDAICBAECAQECAQYKAQMJBAEIAgIJOQEEBQECBAIEAwIBBAMDBggGBQMCAQECAQEBCQYMAgMCBgUBAQUCCAQDBAIEBgQDAQIBAQEEBQ4aCwEIBgQFAQEFAQECBgQCAQcFAwEDCAEEDAQCAgECAwQCAwQOASQDAhcDAgEBAgIGAQEHAkcBAgkDAQQEAQMBAQEBAgMDAgEFBwQBAQEBAQECAwMBAQoDBAIDAgEDAQECBQIOCAEQBgMfBQgBAQIHAQIIAQEBBAQBBQUDCAECBQcDAQEBAwICAQMCAgECAwIDAQsGAQIDAQEBCAMIAQENAQkBAgEFCgEBAQQDAQMBAh4CASACAQEYAQIKAXwDCgECAwIBAQIBAQECBgEDAQIGBQEBAQQDAQEBAQECBQIEAgcBAwMDAQEBAgEBBgIJCAQEAQEBAQIKAgIDBAICAQIJAwUCAwQBAQMFAQEBAQEFBAICAQEBAgYEAgYEAgICAQMfAgMBAUgCCwMIBQUCAQISDQEFBQEGAwQBAQECAgEEAQIRBAEBBwIFBg4JCQEBAwEJBgIFCQIDBwMCBwoBAhQBCQwBBAICCgcBAgsBAggGFwIFAQkGAgEBBAIDAQEKBAIIBgEFBQQCAgQDDQYIAgEBAQMBAQICBgICBQQBBgcCAgQiEh4EAwEBAgIEAgQKAQIHDwMGAR4PBQISCQcQESUOAQYFBAEEFQ8FAQEPBwUSBxQRAwMDBBIgBQYBAgMBBg8CAisB2gQBAQEFIBkBAyEvDAYCBwoGAgYhMQEGBCAZCQICAgMFAwICBwIGARAIAwQBAgEBAQMfGCklCAcEAQIDDw4BAQgCAQQCIzACCQYkFgoCAgICCgEBAhsECwECAQEDBAcEAwIDBAMMBwMFEAgYAgQFAwIDBwEEAwIBCwUJIwYEDRUCAgECBAEIAgEBAwICCAEFAQYBAgQIBgMGAgcGAwEBAycFAQgBCA4PFgcDCggEGQMHAgcFAQEIAQYGAQcFAxcNAQUBBh8KDAQSCgEBAgQEAgYFARkBAQUFAQ0IAQMBCAEHAQIEAcgTGAgLCRAIBQcBCQcEAQEBAgkxAwYEAwYFGBUBBQMDAggQHgYMBB4PAQECBgEIBgoPBQ4IFhIBAm8UFAYHAgYBCxUXAQEIAgEGAgEUGgoMCQkREQoGBAIBAQQSAQUHAwMIBgoMAwwGARISEBwKCEoBKQFXAQE1HwsICAYNBQgGEx0QIjEZAgESAgEgGQsSAhYPBgwHDwIGAwcFBAYREQMBDAICAhcKBAYCAQEBDggLBQ0TCRIYHgICATIECwMDBgEHFAYBBQEVAgEBHxwTDQEMBA8NCQ8BBgUZCAUBAgcJEBMcAQIXCgk+WAEBwAUHAgQEIgECEzIuBQEBAgIEOQIGAgcMBwUCAQ0BFwYCBgECBAsHBAYDGwgBAwEBIyg7CQsRCgoCASsBFRAWAgMFAQYIBAoHCQMGBQYCBAkTERQBDxMaGCAKAQcHDyIUCxQGDQsHAgQBAwIEBwIBBAULFR0XCwgKEAUOCBMVBAgIBAsJBQcMEzASFQ4KBQEDIxcgEBVHEQUHAwcGBwECBgUBFQ0SARocHRoHCQEBBgcNEA8kDwsGBAgGAQMDBQsDAwIGAgILEx0JAQsIBRMGChMUCRYTBAcHBAYBCAUIDRU0KREHCB8bHhQXAgEXDhgDBQEHCQYKOv7tAVRiBwcBAwgWDAoIAgQEBwYGCRwcCgQBCAUOBgUBAgUKChAIAQEDBAURBwUIDgsQEQ8MBAMJAxQvAf32HzYgIDYfBAEfNiAgNh/7/wQBGSMjGfv/GSMjAkIF/k4FOBMTFRZ1T01NUDwQOpNISm4UFCEzAhMBLSw7UE1OTzwFAbUFKywPASgn/mE5lEhKOCksXAMGBwIJAgEDAQEDBgECBwQEAgEDAQQFAgECAgUCBwUEAQoDAQMCAgIBAgIDAQEGBQMCAgMEBAUBAgECBgQCAgIBAgIFAgEDAgEBAQEEAgEDAgEBAwYBAQEBAQEBAwILAQIBAQEvAQEBAgEBBgUEAgsDAQUBAgYDCAQEAQECCQgGBAEBAQECAgMEAwkLBAUICAUEBQsDAQIDBAEOAwICCAoFBAEGAQQBAgUDAggCBAIBBQwGAwcCAQgDBAsFAgUFAwYHAQYEBgUODgcBBg4ECwUKAQUGBAcFAQQECwMDAQEBAgEhAQUMCAMFAwQDBgQDDQ8NAgcIAgIBBwMLCQgHAQEIBAQEAwUEAgMDBgIBBQQCAgQBBgEBAgMCBAICAQEBCAkCBgIBAwUCBwkBAQEHBQQCAgICAgQGDgQDAwMHBwICAQMFAgIDAgEBAQICAQQEAgECAQEBAgEBAgMEBwIDAgUFBgEBAQEBBAEEBAEDBAEBAQEDAQEEBAEBAQEHAQMDAQIFAgUGFgQBBgICAQEBAQEDAQEBAQJCAwQCAQQGBQQDAQEJEhoMAgUICgYIAwIEAwQHDwQBAQIFBQYBBAMGEQQBAQIBAgUHAQIBBQEBAQgIAQQCBwMBAgIBAQEECgkBAgICAQoCARMJCAIBAQMFAQQBAQEEAggFAwUFAwEDBAEBAQEBAgcIDwEBAgEBAQQFAQIDAQIBAwkJAgYCAwQEAgIFARkQCAgCAgECAgUDBAQBAQICAgIBAQEDAQcCBAQCAgIEAgEBAQMBAgYBBQICAgYIBQEDAQEBAgIDAQEBAhEBAQc5AwEBAQIDBAYKAQcFAg0DBgIDBAIEBwEBAgEDAwcHAwICAwQCAwICAQEDAhMDAgIEAwUDAQEBAgIEAwECAgEBBAQDAgkBAQICCAQEAgMEBAYCAgEGCAkBAwEBBAUCAQEBAQICAgEDCQEBAQEBAwEBAgMGBAIdNgECBwEEAQQFAQQDCwUJBQMBAQEBBAILCQIFBQIBBQIGBAsEBgcEAQQBAR4CBAkFBAcHBAYBBAICAQQFAwYCAgECBAMEBAIBBQgEAQEBAQECAQEBAwkCAQICAQIBAQYEBQQEBQQMAgQFBQgNAgECBAEKFwUBAQIGBgUBAw8BCAIFBgQQGAMCAQQBCQ8IAwYBAQMBAwkBAQEBBQIKCg0JBQMBFBIBAQUIBQUBBgECBQMGAQMCAQECAQIGBQIEAgQKCAkCAwMBAQICAgQEBAMDAQYCAgsJAgIBCjwTAcwEBAIEAQUCAQECAwICBAQBAgIBBgEDDAsDAQgBAgEEAgQBBAETAQECBA8DBQQDAQEBAwEFAwICAgMDBQoGAg0HChQDAwQCAgEDAwEBCBEEAQkMBwECBAoCBQYGCgECDQgEAQQCDwgGCw8JAQEBAQECAQELAQEBAgMCAQMCAwQEAwUECAMBAQECCQQFCgQFBQECAw8HAgUEAgICAgEFBggCCAIEAgIEAwECBgUJCAYEAQECAQICAgUEAQEKCAMFAwIBCAkF/t0ECwICBAICAQIEDgMJEwUBAQEBDgYBAwMEBwUDBQIEBQUCAQkBAgkFAQcCAQEEBAUCBAEBBQMDAwECAQQBAQUDAwYDARIMCAoEBgMCAgMFAQoLAgwFBgEFBQgBAQEBAQIBAQUOFgwHCgUDDAICAQMDAQIBBwcDAgIDAgMBAQkVBQQBAQEBBQMDAgECAQEBAQEDBAIBAwIFBQEDAwICBg8FBRcBEQoHDAYKBAEIDAgDBxMHDAgBAQIEBQEBAQO+AwUBBgQLAgEBBAICBgMBAgMJDAgGAQICAwEBAQEBBwIEAQEEBgcEBgQNBwMCAwMDAwEEAQQKAwYEBgEBAQEEBAgCCxMCAgIGAQEGBQICAQEBAgEBAggGAQIBAQEDAQUCAgEDAQUBAgEBAQIBAQEBAQUDAQcHAwgEAQMGCAQCAwEBAQQDBAMBBAEDAwYCBAIDBwIDBQIDCAEBAgEBAQEBCAsHAwEBBAQCBQMBAQEDAQMEAgYEBgcFCgUEBwIFBAEBCgEEAQoOCAEBAQQHDAQCAwIFAwEEAwICAwUHEgMCAwIFAwUECgQHAgUEAQEDCQEEEQwOBAIIAQcDAQQDAQIBBAEBAgIJBAIBAwMDAgYEBQEMCQ4IAQYCBQMDBgcEAwELCAUHAwQFAgMBBAYIAgEEAwIGFAYKDg8CBAEBAQMEAgEIBgMHAQQGAwMJBAgGAQYBA7sBCgEBAQEBAQEEAQwFAgEBAgYPAwYEAgIFAgIBAQEDBQEFCAIDAwQLCwsQAgEBBAEGAgQBBQoSAQEDAQEDBgELCwEBAQMBBAUCAggHCwEBAg8JBQECDQgHAQ0IAQoGAQEBAQECAQIMAQECAQIDAwQBBAsGAgQCAgMBAgMIAwIBBgoBCAMDBhMBAQcDAwECAgEEBwUEAQEDBgEEBAECAwEBAQMGCQkBAwIEAQICBAECBAMNAQkCAwIEAQcMBwgCBQMBAgQBAQYFAQEBAQQFAQcCAgEBAQEBAQICDg4IDQEDAgUBBQUCAQEBCAQIAgICAQEBAQcBAQEBBgQBCQgBAQECAQMBAQEDAQEBAggMBAEDBwMIBQIDAgEBAgEBAQwFBQEBAgEEBgMIBQECAQECAwYBBAEDAgQGBgIDAQEDBAEGCQIDAgEDBgcMDAQCBgUBAwEBBQEGBAECAQIBAgIEBAYBBQEBAwEBAgUMAg0HAQIEAgMFBQIHAwUIBAgFBgcBAwEBAQEEAQIEAgQDAQEEAQECBRICAQYCDwsDAQEBAwIDAQIDAQEEAgECAQIDBQQGBQEICQIFAQUCBAEDCAIBBAkFAgEBAgMBAwMBAQMGAQQHDA0BAwIEAgEBAQUCAgQDAQEBAQIBAgQBAQEEAggCAQICAgIBAgECAQIBAwIGBQIBBQMDAQIDAgcIBAEBAgEBAgEBBAIJAw4CBAcGAwcFAQEBAgQBAgIBAQEBAgEFCQEBAQYEAgEBAgEEAQEMAgEBAQECAQMBAQEBAQIFAQgEDQYBAQMJAwEDBwEDAQECAQEBAQEHAgMCBQgFBgkBAQkHAwIBBwEBCAUKCQQCAQIGAgIBAQEDBgEhAQEIAgECEAwLDgIEBAIFBgIDAQEBAQEBAgICBAEDAQMBBAwRECQGAwEBAQUBBwMGBQoFAwIHAgIDAQIHEAUJAQQEAQMGAQICAQEBAQEHAQEIBQMCAggUBwMDAgECCAgCAQMCAgcCAQMOCQcFAQoIAgMBBAMBBQMBAQIIBQEHBwYBCRUBAgECGwEDAi0BRQIIBggFBQICAQEEAwEBAQcFAwgFAgEBAgQBBAgEAwECAggGARYDAQICAwIDAwMBCgkDBQUGAQEGBgEEAgEEAQIEAgEEBgIBAQICBAEDAwECAwgIAggDAQYBAQwBAgEXBAYGCQ4HBQ4HDg4FAgQEAQUBAgMBBgQKCgEKAQEBAwMGBAYBAQMDBQEGAQcBAwIEAQECBgMHAgMHAgcDAwwDBAQBAgUFCQEEAQQOCAoHBAQBAQIBAQIBAQEBAQEDBwMJAQEGAQUKAgEEAQgDAQIDAQYGAQEBAQEBAwIBAgkCBAECCB4FAwEICgQGBggFGQ0EBgEGAwMLCAgFAgEDCAUEAwECAwQOAwECAwECAQEBAwIBBQcFAQMCBAECAwICAgIGBwQCAQICAgICBwUDAQEBCwUEAQEFAQIBCQYBCQgFBwEBBAEBBQYBAQIBAwEBAQEFAQECAQIEAwMDAgECAwIBBAQBBAIBAgIDAwQEAwEBBAMFAQIFAgQCAQIHAQQBAQUGAQUBBAICCCIBCQoEDAEKAgIBBQEBAQcFBQ0CAQUBAVoCAgEBAgsIBgEDBAEMGRAJAQECAgYEAQQCAQYDAQEBCAUCAQIGCQQCAQIBAQMBAgMCAQEHBgQBAwMBAwMBAQYFAgEBAQMPBgEBAwYBAgIHAgECAQQNBwIDAwUCUQIGBQIBBAIBAgEBBQECAQ0FAgQFBwICAgEGAQEHDgEBAQIIAQUEAgECAQwMAgYBAgQEAwECAgoUDgkCAwMBBAMBAQECAwICAwQLAwwJAwEFAgMEAgEBAQEBAgIKARADBAUBAQEJBzwBAgECAwEBAQEBAwQDAQEGBgUCAQEBAwIBAQcCAgEEBAIFAgEHBgMFAgQNAwQCAwIVAgMGBwcBAQMEAQECAgMBBwIBBAMEBQMGAwQEAQEBAQICBAcHAgMBBQoCAQEBAQEBCRACAgQDAwEBAwEBAhoCAQEFAgEBCgE1DAsCAgQBAQECAgIMBAECCgkEBQEBAgQFAgEBAQICAgQHBgMHBQIGAwQEAgEEDBoEAggGBQMCAQkOAwEFAwEBAQIBDAgDAQQIAQQEBwUFAQECAQEDAwQCAQIBAQEBBAIFAwQDBQMHBAEjARoDAUsBCBAIBQEBBAsUBwEEBgcIAQEBBAICBAMBBAUDAQgCBgoNBwIDAQECAQIJBQgFAQEEAwIBBgEBDgEHAQEDAgcHBgEMCwQCARYPAgUIBQ0BAQMFAwMBAQQCBgYBAwcBAQEGBwoHBAQBAwEBAQEBBAMEAgIDBAIDDP7lCAEDAgEFAwQIAwoWAgIGIQgIAgEIAQIGBgoBVA0DBA0NBgEDBgcBAgUEEQULAQEBCAEHAQEJBgQKASIBBE7DHg8CAQcIIxcBAQsEAQEHBAIBAQkFBB0fCQ0QFBYUDQYEBAcHHDcSAyIRHg4DAwIDAQUlFAMJAgIBAQ8IAQIBAgkFBiIaDQ8BERQMLAYDAQEBA00WEwECAgMFBg8GBQUBBQEBBgMBAwYrEwECAwMHDgMFBQQBOQMEIh4aRiECBQEDAwQDAQ4BBAEBAQcGAQcEAQIBBAECAwIbAgEEDAUCCA5dCyYUAgMHBgcHAgMCAgsBBAMHAgIYERYMAgcBBwETFgMBAQoQDBQMAwUBEQQBAQIBAQEDCwECAgEGDAEDBBcUFQ8CAwYNAQH+tBsKBAcHBQkBDhUOBwMEAwkCBAIEBAMSCQEDCAYBCwgUBAoBAQEKFAMVCRAHAgMJGQMCAjwFAgQIDgwBBQcKBgMBHAsFAwcWDxYKDAQDAwERAxcKCxYJDwYBAwgXCBIIBjABI3oLHzUBAQkBBAkGKh4PIgICDgsHEQEGCgYOARgeDAsHCwEEBwoHHBMDBjALARYREQICAh4RBQwCAQoICwsYEwoJDQMBBYQFBgYKDRoIBwYjHwEBBQwJDQ4gHgwJCgUbEhAMBgoHCggJBQUiAh4RBRQiAQFkCgIBFAIMCQcQAgIGBAUOAQUBBAcDAgMbAQMTGAEEAgEMAQQIAwsDAQ8FAQIBAQEVCAoZAQQGBAVpGBwTDgEECw8HHRkNDAcHDhgZGAYDCgYMExkrExMLCgUDCSEVIhoHBAkLCgIGAQUDBAEDAwYHCQcKBQEDBgECAwERCgUHCQ0PESMZHwECBwkOExXwEQUHCxcWGwQCCA0DAQ8QFCscDwoEAgkHAyIPIA0LCggFBAkKBgMHAQEBAgEDBgQDCAQRBQoHBAYBAgIECgsFCAkOISYSJgULCxcXIBscERALDwceGxEL/spAAgHfBgcIDBMOBAECAgYFAgQRGQ4JEwQLBgUGCw4GBgMBAgMCDwYEAwYKCh4QDAkYLgMBAiAgNh/9CR82ICA2HwL3HzYgOSMZ/QkZIyMZAvcaIgAAAAcAAP9rA+cDSAAdADkAXABsAHwAjAD8AAABIg4BBw4BFREUFx4BFxYyNz4BNzY1ETQnJicuAgcyFx4BFxYVFAYHBgcGICcmJy4BNTQ3Njc2NzYFFhcWIDc2NxUGFTAxBgcGBwYHBgcGJyYnJicmJyYnMDE0JwUVBgc0FQYHBgcGBzU2NzYlFhcWFxUmJyYnJic0FTQnBTMyFh0BFAYrASImPQE0NgURFAcOAQcGIicmJyYnJjURFhcWFzMWMxUjIgYdARQXIyIGHQEUFjsBMjY9ATQmKwE2PQEzFRQXIyIGHQEUFjsBMjY9ATQmKwE2PQEzFRQXIyIGHQEUFjsBMjY9ATQmKwE2PQE0JisBNTI3Mzc2NzYB+GKuhygWGS8niVZZwVlWiCcwDQ0WKIavYl1VUnseGwoOIDt1/mF1OiEOChohOz1SVf6mJEN9AaZ9QyUFBAscKzBDRU9WVk9FQzArHAsEBANvBAEECxYwWoePYDH8rBkya6KbYzEWCwQEAYd+BQYGBX4FBgYB7RsefFFVuVZRPjshGiArbKcDLRaqCQ0EFAQEBARKBAUFBBIElQcaAwUFA0oEBQUEEgeXAhkEBQUEUQQFBQQQAg0JqhQoFAWcXioDSBgtIBAqGP2SLiUfLQwNDQwtHyYtAm4YFhMRIC0YNwwLKRkUEwgOCRQMGhoLFQkOCBQTGhMVCwzRFQ8bGw8VNwcIBwgRDA8JCgMFBQMKCQ8MEQgHCAZRKAYIAQEGBw4OGAk9CRwPDw8PIAc9BxwODgcGAQEIBg0FBFAEBQUEUAQFQv7jExQZKQwMDAwVExoUEwEdEQwdCAI0CgdXBgQFBGMDBQUDYwQFBAZFQwcFBQRjAwUFA2MEBQUHQEUEAwYEYQQFBQRhBAYDBFcHCjcCAQkbCwAAAAUAAP9rAugDSgARABwAHwB/AI8AABMiBhURFBYzITI2NRE0LwEmIwUhFRQXHgE7AREhARcjBSIGHQEUFjsBBxUjIgYdARQXIyIdARQ7ATI2PQE0JisBNj0BMxUUFSMiBh0BFBY7ATI2PQE0JisBNj0BMxUUFyMiBh0BFBY7ATI9ATQrATQ9ATQmKwE1JzMyNj0BNCYjBzMyFh0BFAYrASImPQE0NiMKEA8KAqwKDwfeBw7+TgGYAQIOCcj9hgHLkpL+/wgMDAhiAZcIDAESCAhDAwQEAwsBhRIDBAQDRAMEBAMLAYMBEQMEBANDCAgLDAiXAV4IDAwIwpwGCAgGnAUICANJDwv8VQoPDwoCyQoI3gwy0AMDCQr9bwNVkp8JBn4GCARLDAhgAwIHUwcEA1MDBAIDTEwCAwQDUwMEBANTAwQCA0xMAwIEA1MDBAdTBwMCYAgMSwQIBn4GCRsFBE0EBQUETQQFAAgAAP9qA+YDUgBSAGQAawB0AIgAkACYAKgAAAEVIxUmJyYjIgcxBgcGFxYXFhcGBwYHBgcGFBcWFxYXFhcGBwYHBhYXFjY3NjcVMxUzNTMVMzUzNTM1IzUzNSM1MzUjNTM1IzUzNSM1IzUjFSM1BTIWFxYXFQcVIgcmJyYnJj4BBSERIRE3JwciFQcGByYnNgcWFwcGByYnJicmJyY0NzY3Njc2JRUjJyYnNjcHFh8BJic3NgcWHwEVBgcOAScuATc2NzYCAyhVRE8rFwsLAgEGDCYgIEo2RSIRDQsLDREiRTZdJiEnDggCDBBBJk5pKA/KDybR0dXV0tLPz9TUJg/K/rUTPCRFXAFTZSceJQwFAg4BMwEY/ugGBh0BASA1JiRQYisnESAOWEI9Jg4MCAgMDiY9NAERBRMhHhs7YBQrCEM8Dxs0QVcLa1EmNgkGAQYMKBsDUs45SigxCwoTEBgtQDMsCA0QEwgNDRsNDQgTEA0KNDZAMBcjCw4QFy5eT8jIzs4pD3UPXg5yD3UOIMjIznMYFipPCQlJCTQxPyoXGA5q/dIB5wsGcQEBIj8sLwgJOSwWKBMIDw0UBwwKDQoMBxQNDAi8EyMiIkFuFy0JAgQTI0QFAgtUYDAXDQkGFxgqQi0AAAAABAAA/2oNuwNSAAkAEwAdACMAAAEDIQUDJQUDJSEBAyEFAyUFAyUhAQMhBQMlBQMlIQETIQcTJwvIdv6DATV2ATQBNHUBNP6D9bV2/oMBNHUBNAE0dgE1/oMEfXX+hQEzdQEyATJ0ATL+hQRuXwE5/WD7A1L+g+v+guzsAX7rAXv+g+z+g+zsAX3sAX3+her+herqAXvqARr+z7/+yr4AABL/9/9qCKgDUgAOADMAWQCIAJcApgDOANEA+AD7ARgBMgFKAWYBfQGYAaEBqgAAAQ4BFRQeATI+ATQuASMiJzMVFzcXBxYXMxUjBgcXBycGBxUjNScHJzcmJyM1MzcnNxc2NycdAQYHJwcXByMVMxYXBxc3FxUzNTY3FzcnNjczNSMmJzcnByc1BzMVFzcXBxcWHwEzFSMVBg8BFwcnBwYHIxUjNScHJzc1Ji8BIzUzNyc3Fzc2NzMXIgcOARUUHgEyPgE0LgEHMh4BFA4BIi4BNTQ2NzYlJxcnMycXJzMnMyczJzMHMwczBzMHNwc3Byc/BR8GBQc3Ax8BIxcnFyMXIxcjNyM3IzcjNwc3BzcHNxcPBi8FJTcHASIHBgcOAQcOAQcGFhcWNjc+ATc2NzY3NicmJyYHMhceAQ4DBw4BLgE3Njc2Nz4BNzY3NjclIgcOAR4DFx4BNz4BJy4BJy4CJyYHMjMWFxYXHgEXHgIGBwYmJy4CJyYnJjY3NiUiBw4CFBcWFxYXFjI3PgE3NjQuAgcyFx4BFxYUBwYHBgcGIicmJyYnJjQ3PgE3NhMiBhQWMjY0JgcyFhQGIiY0NgdhGyIbLjYvGxsuHBUkcyZEUkUKBV5dBg5CUkcKEnMiRVFCCgdbXBBCUUUKGAIQEEVUQxBcWwcKQ1RFIHcOC0hVQwwIXV8FCEVVRSNzbyhET0QBCgQBXFwGDgFCT0cBCBQBbyVET0IKBwFaWxFCT0QCCxYCNxQUHCMcLzgvHBwvHBstGxstNi0aIRsS/wBMAU0BUgJXAjABMwFsAWkBMgEsAk0CQwIoEyhGUC4ya200MVpUTkz8lHxAFE1OAVMCWAIwAjQBbAFpATEBLAJMAkICJxUoRU4uMmpuNDFaVVBOA2t7O/vwFiA6Q0aKOzdLDg4iLCh3QkeJOzcmJwsOEBEsExsaECUbGEhviUVBcEsbDA0jJDc5iEVBOREh/iUbEywiHEpxi0dCeCYsIg4OSTk6i4k6IBYBASASOEJFiDk3SRYbJCdvQkWJbyQlCgwbJBEBCR8cGScVCwoTFRgbQRsZJgoLFSU2IBgXGSMLCgoLEhIYGC4XGBISCgsLCiMZFhIfLS0/LCwgGSMjMiMjAbkMNyEdMh0dMjsyHcdnEUpXShATfBoaR1dMBwVoaA9KWEcRHXwnR1hKBwhpAmYECktbSCSAGxBIW0sOaWkDCE1aSBQdgBMNS1pKD2gEZxFJVEoBEhEBeAIaGQFHVEwBBgZoZxFKVUcBEhsCeClHVUoBCAdcCQ03Ih4zHR0zPDMeBB0xOjAdHTAdITUMCecoASQeARQGAwEHBgcWAR8BGCAYIBcIBgcBAwcUHyQoDw1r/O0lIBsBEQQBAwsICQEZASIBGR8aIhoKCAsDAQURGyElMRFsAqAFCRoaUDAsWikrOgoJFBkbTzAsLjElKR8cCgQQBAcvSFdaUBoZEg8wIicpLC0uTxoZCQMCEAQJO1NaXFAbGRQJCTsrKFotL1E0CQUQAgMJGRtOLi1ZRy4HCBIZG09aLC8hJC4HBMUkInaZrE5MOz0fJCQhdkxOrJl0SBEdIXBKTapNSzk3IB4eIDc5S02qTUpwIR3+uDBEMDBEMBEmNyYmNyYAABQAAP92ByIDPwDcAOsA7wDzAPcBBwEYASABKAEsAUQBTAFUAV0BZgF0AYIBigGTAaEAAAEiBgcGByYnJiIHDgEXFhcWFwYHBgcnNQcGJyYvAgYiBw4BFx4BHwEWFxYXFhczFhcWFxYXFgYHDgEHBiMGJyYvAS4BJyYHBgcGBwYHBg8CDgIWHwEyHwIyNzM2FxYXFhcWHwEWFxYXFhcWFxY2NzY/ATY3NhcWFxYHBgcGBwYHBicmLwEmJyYHIgcGBwYHBg8BFBYfARYfARYfARYXFhcWFxYXFhcWFxY3NjcvASMnFjc2NxYXHgEyNzY3NjcWFxY3PgEnJicmJzY3PgEmJyYiBwYHJicmJyYHMhcWFxYXBgcmJzY3PgEFFSE1BRUhNQUVITUFFhcWFwYHBgcmJy4BNjc2ITIXHgEGBwYHJicmJzY3NjcFFhcGBwYHNiUWFyYnJic2BRUhNQUWFxYfARYVFAcGBwYHLgEnJjU0NzY3NhcWFwYHNjU0JQYVFBcmJzY3IgYUFjI2NCYHMhYUBiImNDYFFhcWFxYOAScmJzY3NiUWFxYXBgcGLwE2PwE2FxYXFhcGByYlBgcmJzY3NjcHFhcGBw4BIicmJyYnNgWAIz8UDQkoGSE6FTIkDw8qERsXGRsSbUlSKxlEYBMDBgQCAgEBBQcDEhk3MBtBAh0OFQkUCAMEBAYgEismHiUYMRUTThYnJyM3OjQ5JAsPCQQBAgICAwEDAQogGUILTyY+Gx4ZExUREQgPDSEiGSIeMx4TKwgrEBMLCQQEAwMWGzA2NBgjFS4UKxQiGTlUJhAJBwQCAQUDCQkPFh8SEDUaLRwaIhQpPR80KUVWNWUDAgEuJzkhIgsNFT5GIB0WDQw0GEIuMCYQDSwXGBUWKSAmMBU7IBwuCQ4VHiAjHRsaGA4IPzxIPAoMFTf9iwEv/lcBLv5NAS8BWBwfHCQMBj42ERoqGh8rFwI2GhcrHxwoGhE4RgYLHSwVJP5sOj0iKCEcBAERDAMRIiQmOPycAS8B7TEiDBcZBAQdIzAfKE0kAwMgJirGQDM1PgP+uQMDPisv3CY3N003NyciLi5DLy8BQBMcKA4PIFc/IykLBUT+BjBEBA0kHz0oPA8hBxmRJB4iJEUzDAEiBQo8MyYfEB+EPEELDRg1OhscFQ0LQgM/UT8kMQoEBgQLQC8sNBUbFh4hIt4HAwIBAQYJAQECAQgBBAMEAggJFQwHBAIBAwQIEAcLAgMIAQUBAwIFAgIQAwQCAgcHCQkIAgQDAgECBQQCAQEBAQIBAQEFBQwJDwwNBgkFDAUEAQEHCAYWBBYBAQsIEhoTHBAVEBMEAgQDBwMIAgUBDwcGAwMCAwUDBAEDAQIBAgEBBAMEBgYMBxIZCRECAxYNJAYGXwYKBQo0KEFQKCVEJTkOAwsJC0AvLTMcGBUbMV5ACwQGBAwvKEMlKA0jH0YqLBMdIREyIkJGEQ4OUQ4OXA4OEwIEBAo/SCsyER4zUTcJBQUJNVUxHhE1Lj5ACQgEAhgRHA8YExM4P0QsCxQWERkSDg4OGBMHDw9EJiI+FBUbDhItGSwuMzAWFhhHKzAyKiotMicrLiknKyosJzNKNDRKMw4rPiwsPitOEyAxKyg3EgoGDDdJLjYvLkhCCwQJB3ojKAgdWRgRFBEfD0E0PjQTGBITCRNJHBU2KEREIyY/KDQSAAAC//7/agR4A1IAiwCeAAATMAcGBwYVBhcWFxYXFhcWFxYXFhcWFwYVFBYzMj4BNCc2NzY3FjMyNjU0Jic2NzY/ARYzMjY1NCc2NzY3JwYHBgcuASMiBhUPByMXMxc3Jz8HJxYXBwYHBgcmIyIGFQ8HIycVMxcOAQcmJyYnJicmJyYnJicmNTc0FQEWFwYHBgcmIzcnPwcWAggFBwETBg8YDxcVHB8jJiwrGBcCNycZKxoPIy0jKgwPJzcXFCgZIhcQFhwlNQYRFg0aAxoRGBYNJBUlNBITIh8cFxIKHQECJC4cChAWGRscDQ8BBQwTGSIaKBAJJzcSEyMfGxcSCxcFAx8XJwwVFSgqJiEzMA4XEAYSAQJuByEsISsfGSApHAkRFRkbHQ0PA1IBAgUIDxlOGElxPmBAUS00KjEeEAoHDSU0GCkwFyBNPloENyYYKg1XMkIgFBA3JhMQBwQCAxwDAwUJERI3JwEHFSApMDU4ATc1AjIyLCYbEgQCBxQRGCNDM1cENycCBhUgKTA1OQEBMQEXEgkPHC4pMkvJPW9MGUcYBQEB/gYkF1s5SxwULwIyMiwlHBEFAQAAAAADAAD/agQLA1IAAwAHAAsAABMHITcXBxE3AREhEfDwAvvxH/b2+/UC+gNS4+MG+f0X+AHx/RcC6QAAAAAO//b/agM6A1IACAARACkAQABZAHAAiACdAKkAzgD0ASMBLwE7AAABIgYUFjI2NCYHMhYUBiImNDYBJicuAicuAScmNjc2FhceBAYHBjc+AScuAScuAScuAQ4BFhcWFx4BFx4BBSInLgEnJjQ3PgE3NjIXHgEXFhQHDgEHBicyNjc2NzY0JyYnLgEiDgEHBhQXHgInBicuATc+ATc+AxceAQ4DBwYHBicWNjc+ATc+Ai4BBgcOAQcOAhYFDgEVFBYyNjQmIyInMxUXNxcHFhczFSMGBxcHJwYHFSM1JwcnNyYnIzUzNyc3FzY3Jx0BBgcnBxcHIxUzFhcHFzcXFTM1NjcXNyc2NzM1IyYnNycHJzUHMxUXNxcHFRYfATMVIxUGDwEXBycHBgcjFSM1JwcnNycmJzUjNTM3JzcXMzY3MxciBw4BFRQWMjY0JgcyFhQGIiY1NDY3NgF4IjExRDAwIhkjIzIkJAEVFyE9j5M9OlAPECQwLHlHSZN4Th8kMBgVJRkNDUs6PI5IRXRMGBklJjo8jkhFdP7zIx8bKgsMDAsqGx9FHxsqCwwMCyobHyIZMBQTCwwMCxMUMDMuKQsMDAspLtgXGTAkEA9QOj2Tj3ksMCQfTniTSUc9IkIldkRIjj04TRkZTHVESY47OkwYGQIXFRotQC0tIAwgWBw1PjQGBUdGBgoyPjYKC1gaND4yCAVFRgwyPjQIEgIQCDRBMwtHRgQIM0E0GFsOBjZBMwkGR0gGBTVANRtYVR80PDQIAwFGRgQLATI8NgEJDAFVGzQ8MgEJBEVGDTI8NAEOCwErEA8VGy5BLi4gHywsPi0aFA4B9TFEMDBEMRYkMiMjMyP+xQEHDDxYMjFgKSw9CAYZHR9YYl5XPQgDGgYqIiZbLzFWHhwYDClKLC4vMVYeHBi6JSB3S0+pT0t3ICUlIHdLT6lPS3cgJRY7NzpJTaVOSDo3OzlySU2mTUpyOXwBAwc9LSlgMTJYOxkGCD1XXmJZHh4MBxgGFx0eVTIuXEgqDBkbHlYyL1tIKxsJJxggLi5ALo9KDDU/NQkQWRURMj83BgNKSgs1PzMOE1kcMz81BQZMAkkGBDVBNBpcDxAzQTUKS0sEBDdBMxATXBAINUE1C0sESgw1PTUBCw4BVgETEgEyPTcBBgNKSgw1PTMBEw0BVh4zPDQIA0IGCSgYIS8vQi4DLT8tLSAXJwgGAAAKAAD/aQS7A1IAQQBGAEsAWgBfAGQAaQBuAHMAeAAAASIGByEVIRQWFxUhFSEVDgIHIxUzHgIyPgE3Mx4BMjY3MzUjLgEnNT4CNzM1Iy4CIg4BByE1PgE3ITUhLgEBHgEXIycVIz4BBSEeAhcVDgEHIy4CJyUzFS4BNzMOAQcFFSM+ATceARcjBzMVLgE3Mw4BBwF8JDYI/uYBFzEl/pMBbShDKgPV1QQuSFRJLgPgCDZINwj59wE0JihFKgO4uAMtSVVJLgP+yyY0AQLa/SMINwHKNEgFgRl7BUX+ZwE1AylCJyQxAd0CKkMoAU97MUWPgQVINP4EfgRHTDJIBH6XfjJHkn4FSDEDUishGSU3Bu8Z0wMqQycaKUQoKEQpIiwsIhomOARwAipEKBkqRigoRirvBDgmGSEr/sgERzN+fjJHkidDKgNyBTclJ0MqA9N+BUcyM0cEbn4ySAQESDIaewRGMTFGBAAI//X/agN7A1IACgATAC0ARABdAHUAkACoAAABIg4BFRQWMjY0JgcyFhQGIiY0NgEmJyYnLgInJicmNjc2HgIXHgEXFgYHBjc+ASYnJicuAScmJyYOARceBBcWBSInLgEnJjQ3PgE3NjIXHgEXFhQHDgEHBicyPgE3NjQnLgEnJiIGBwYHBhQXFhceASUGJy4BNzY3PgI3Njc2Fx4BBw4BBw4CBwYnFjc2Nz4BNzY3PgEmJyYHDgQHBhYBuBosGjhRODkoHikpOyoqAUUeJEdTVquNLi8QEyo4Mo+orUdEXxASKzYdGCwdHisuQkenVFFER1UdDhBXiamlREL+xSkjIDENDg4NMSAjUyMgMQ0ODg0xICMqHTkvDQ0NDS8dGzs3GRcNDQ0NFxk5/wAbHTgqExAvLo2rVlNHSjE2KxIQX0RHrahHIlQqR0RRVKdHQi4rHh0sLkJEpamJVxAOHQG4GiwaKDk5UDgaKTspKTsp/pACBw8iI2h0ODouNUYKBx1GZzo5cjA1RwgDHgYwVzQ2NztkIyEODg4yKCxqcWVEDg/ZKyaKWVvKW1mKJisrJotYXMhcWIsmKxpEhFZaxFpWhCMhQ0NCVlrEWlZCQ0ORAQMKRjUuOjh0aCMiDw4HCEc1MHI4O2dGDwYaBw4OISNkOzc2NFcwBggPDkRlcWosKDIAAAAS//X/agUVA1IAQQBRAJYApgC2AL4AxwDjAPAA+QECAQsBGgEqATIBPAFNAVwAAAEiBwYHBgcmJyYOARcWFxYXBgcGBwYWFxY3NjcWFxYXFjI3Njc2NxYXFjc+AScmJyYnNjc2NzYmJyYHBgcmJyYnJgcyFxYXFhcGByYnNjc2NzYFIgcGBwYHBgcGBwYUFx4CMj8CNjc2MzIWFx4BFRQGBwYHBisBIg4BBwYVFxQWFx4BMzI3Nj8BPgE3Njc2NTQuAScmBTYXFhcGBwYHJicuATY3NgUyFx4BBgcGByYnJic2NzYFFhcGBwYHNiUWFyYnJi8BNgcWFxYfARYVFA8BBgcGByYnJi8BJjU0NjU2NzYXIg4BFB4BMj4BNC4BFxYXBgc2NTQvATIWFAYiJjQ2BxQVBxQXJic2BxYXFhcHBgcGLgE3Njc2JRYXFhcWBwYHBicmJzY3NgUWFxYXBgcmJQYHJi8BNz4BNwUiDgIUHgIyPgI0LgIFFhcGBwYHBiInJicmJzYBrCkjIRcJDSQdRWsrEhEvGDQwHC8REis2MUkcLAcIFyEjUiMgGQkNNSJIMjcqEhEvEx4eEy8REis2MUkiMA4NGSAjKR4cHhYQCzA0YEgJDBYeHgKvECAbDxMNCwgEAgEBAgIIBgoWBRELFBcaKgwODQoNChUTGQcIEgoCAgUHBQgRExgPDAIDIzkVEw0LGDMpK/v9JzIbJxMEMCAvHSwdHSwOApMKDiweHysZFzpLBAsuJTL+T0xFMCwaMAYBJQUGHhELGAgvSiYUESAOAwYUGg4UJkMoLCoBAgIqLSgrGisaGis0KxoaK48/Mjw3BQKqHSoqOioqpQECJxcgMi4jCBMIKxRHVh0ODyweAmsXGSsQDw8PLCpHIjUJB0r+ayggOSNDSRABKQMKEyYRCBgWFAINExwPBgYQGSoZEAYGDxv9ez8gCgwWHhw6Hh4WCAdLA1IrJ0UZMAwFDxBGNS85HjAsIjkvNkUICRAFDhwVRScrKyZGGzQSBg8HCkU0LzoXHh4XOS81RggJEAYQPyhGJisYIiRBKz0QGTEXJyJBJCKLBAUFBQcFBgQJBScHDAkGBQsCBQMFDw4OJRkVJA4MCggECwoOC3IGBwEDAQUDCl4DHBgUIx0qJ0U0EBA7AgsFDFdiIx0rIjZVMgcCGQIHMVc0HhY2NDs0EAcLCxojGBsPIFYqHzcUCgcNBBUiFAwKFgkuMS1ODRIIDBQgGRoeGyMTCiMJHhsZLBotNS0bGy01LRoNLS82JzkxEiMPKz4rKz4rIQUMEhYoHBUbLCcZZFMCDAQODjMoLDUkKRYeNC0qGBoGBw4GEi4+MxkcEyERIhlELR8zBhEHBA0NDiIIEBsoGhIHBxIaKBsQCBkdCy8gQSQiIiRBGRgZAAAABQAA/2oD8wNSAAUACQANABEAFQAAGQEhNSERAREzEQURMxEFETMRFxUzNQPz/GYBPZr+gKUBHaU3pQNS/BhYA5D+4v3mAhrQ/rYBSjf+7QETpW5uAAAAAAQAAP/wA6wCzQATACcANwBHAAATIg4BFREUHgEzITI+ATURNC4BIwUhMh4BFREUDgEjISIuATURND4BFyIGFREUFjMhMjY1ETQmIwUhMhYVERQGIyEiJjURNDbSKUQoKEQpAkUoRSgoRSj9uwJFHzQfHzQf/bsfNR8fNTwmNzcmAgsmNjYm/fUCCxgiIhj99RkiIgLMKEQp/k4oRSgoRSgBsilEKCIfNR/+Th81Hx81HwGyHzUfMTYm/oImNjYmAX4mNiIiGP6CGCIiGAF+GCIAAAAAAgAA/78DiwL3ABMAHAAAAQ4DHgM3PgI3Byc/AS4BCQEGHgE3AS4BAqIsUDsdBihDVC0wVTsLnoI6piJY/u/+mwMfMRcBXR8vAvMDKEVWWlE7HQMDL00wN0uMOiAg/p3+kxYxHwMBZBI3AAAAAQAA/+IDiALxABMAAAEGBwYHFh8BBwYHFh8BNj8BFzY3A4hRULRbFCciv6pUBw4LWbKyWDFaAvEpKForEiciv6pVBw4LWrKzWGCtAAIAAP/dA4oC8wARABUAAAEGBwYHFh8BDwI/Axc2NwEPATcDilFQtFsWKxvKzyj3AiDJWDFa/igboBoC8ykoWisULBvMIvcoD8XKWGCt/nKhGqEAAAIAAP/iA4oC8QAGAAoAAAEFFwEXARcFFSE1A4r+UVv+RSABvFn9zAFqAvHWWv5BIAHAWZ4rKwAAAAASAN4AAQAAAAAAAAA1AAAAAQAAAAAAAQAEADUAAQAAAAAAAgAHADkAAQAAAAAAAwAEAEAAAQAAAAAABAAEAEQAAQAAAAAABQALAEgAAQAAAAAABgAEAFMAAQAAAAAACgArAFcAAQAAAAAACwATAIIAAwABBAkAAABqAJUAAwABBAkAAQAIAP8AAwABBAkAAgAOAQcAAwABBAkAAwAIARUAAwABBAkABAAIAR0AAwABBAkABQAWASUAAwABBAkABgAIATsAAwABBAkACgBWAUMAAwABBAkACwAmAZlDb3B5cmlnaHQgKEMpIDIwMjMgYnkgb3JpZ2luYWwgYXV0aG9ycyBAIGZvbnRlbGxvLmNvbWJwbW5SZWd1bGFyYnBtbmJwbW5WZXJzaW9uIDEuMGJwbW5HZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuaHR0cDovL2ZvbnRlbGxvLmNvbQBDAG8AcAB5AHIAaQBnAGgAdAAgACgAQwApACAAMgAwADIAMwAgAGIAeQAgAG8AcgBpAGcAaQBuAGEAbAAgAGEAdQB0AGgAbwByAHMAIABAACAAZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AYgBwAG0AbgBSAGUAZwB1AGwAYQByAGIAcABtAG4AYgBwAG0AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAYgBwAG0AbgBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AQIBAwEEAQUBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAWsBbAFtAW4BbwFwAXEBcgFzAXQBdQF2AXcBeAF5AXoBewF8AX0BfgF/ABpxdWFudG1lLWN1dHRpbmctc3VicHJvY2VzcwV0cmFzaB1xdWFudG1lLWRhdGEtcHJlcGFyYXRpb24tdGFzaxBnYXRld2F5LXBhcmFsbGVsH2ludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1jYW5jZWwxaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW5vbi1pbnRlcnJ1cHRpbmctbWVzc2FnZRhzdGFydC1ldmVudC1jb21wZW5zYXRpb24uc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1wYXJhbGxlbC1tdWx0aXBsZQtsb29wLW1hcmtlchJwYXJhbGxlbC1taS1tYXJrZXIjc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1zaWduYWwvaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW5vbi1pbnRlcnJ1cHRpbmctdGltZXIqaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLXBhcmFsbGVsLW11bHRpcGxlJWludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1jb21wZW5zYXRpb24LZ2F0ZXdheS14b3IQZW5kLWV2ZW50LWNhbmNlbCJpbnRlcm1lZGlhdGUtZXZlbnQtY2F0Y2gtY29uZGl0aW9uO2ludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLXBhcmFsbGVsLW11bHRpcGxlFXN0YXJ0LWV2ZW50LWNvbmRpdGlvbiJzdGFydC1ldmVudC1ub24taW50ZXJydXB0aW5nLXRpbWVyFHNlcXVlbnRpYWwtbWktbWFya2VyCXVzZXItdGFzaw1idXNpbmVzcy1ydWxlEnN1Yi1wcm9jZXNzLW1hcmtlch1zdGFydC1ldmVudC1wYXJhbGxlbC1tdWx0aXBsZRFzdGFydC1ldmVudC1lcnJvch9pbnRlcm1lZGlhdGUtZXZlbnQtY2F0Y2gtc2lnbmFsHmludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1lcnJvchZlbmQtZXZlbnQtY29tcGVuc2F0aW9uFHN1YnByb2Nlc3MtY29sbGFwc2VkE3N1YnByb2Nlc3MtZXhwYW5kZWQEdGFzaw9lbmQtZXZlbnQtZXJyb3IjaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLWVzY2FsYXRpb24eaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLXRpbWVyFnN0YXJ0LWV2ZW50LWVzY2FsYXRpb24Sc3RhcnQtZXZlbnQtc2lnbmFsEmJ1c2luZXNzLXJ1bGUtdGFzawZtYW51YWwHcmVjZWl2ZQ1jYWxsLWFjdGl2aXR5EXN0YXJ0LWV2ZW50LXRpbWVyE3N0YXJ0LWV2ZW50LW1lc3NhZ2UXaW50ZXJtZWRpYXRlLWV2ZW50LW5vbmUdaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLWxpbmsUZW5kLWV2ZW50LWVzY2FsYXRpb24HYnBtbi1pbw9nYXRld2F5LWNvbXBsZXgSZ2F0ZXdheS1ldmVudGJhc2VkDGdhdGV3YXktbm9uZQpnYXRld2F5LW9yE2VuZC1ldmVudC10ZXJtaW5hdGUQZW5kLWV2ZW50LXNpZ25hbA5lbmQtZXZlbnQtbm9uZRJlbmQtZXZlbnQtbXVsdGlwbGURZW5kLWV2ZW50LW1lc3NhZ2UOZW5kLWV2ZW50LWxpbmsgaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW1lc3NhZ2UlaW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LWNvbXBlbnNhdGlvbhRzdGFydC1ldmVudC1tdWx0aXBsZQZzY3JpcHQLbWFudWFsLXRhc2sEc2VuZAdzZXJ2aWNlDHJlY2VpdmUtdGFzawR1c2VyEHN0YXJ0LWV2ZW50LW5vbmUjaW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LWVzY2FsYXRpb24haW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW11bHRpcGxlNGludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLWVzY2FsYXRpb24daW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LWxpbmsmc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1jb25kaXRpb24LZGF0YS1vYmplY3QLc2NyaXB0LXRhc2sJc2VuZC10YXNrCmRhdGEtc3RvcmUnc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1lc2NhbGF0aW9uIGludGVybWVkaWF0ZS1ldmVudC10aHJvdy1tZXNzYWdlMmludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLW11bHRpcGxlMGludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLXNpZ25hbCFpbnRlcm1lZGlhdGUtZXZlbnQtdGhyb3ctbXVsdGlwbGUkc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1tZXNzYWdlDWFkLWhvYy1tYXJrZXIMc2VydmljZS10YXNrCXRhc2stbm9uZRNjb21wZW5zYXRpb24tbWFya2VyJXN0YXJ0LWV2ZW50LW5vbi1pbnRlcnJ1cHRpbmctbXVsdGlwbGUfaW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LXNpZ25hbDNpbnRlcm1lZGlhdGUtZXZlbnQtY2F0Y2gtbm9uLWludGVycnVwdGluZy1jb25kaXRpb24LcGFydGljaXBhbnQZZXZlbnQtc3VicHJvY2Vzcy1leHBhbmRlZBFsYW5lLWluc2VydC1iZWxvdwpzcGFjZS10b29sEGNvbm5lY3Rpb24tbXVsdGkEbGFuZQpsYXNzby10b29sEWxhbmUtaW5zZXJ0LWFib3ZlEWxhbmUtZGl2aWRlLXRocmVlD2xhbmUtZGl2aWRlLXR3bwpkYXRhLWlucHV0C2RhdGEtb3V0cHV0CWhhbmQtdG9vbAVncm91cA90ZXh0LWFubm90YXRpb24QcGxhbnFrLWRhdGEtcG9vbBxkYXRhZmxvdy10cmFuc2Zvcm1hdGlvbi10YXNrE3BsYW5xay1zZXJ2aWNlLXRhc2sXZGF0YWZsb3ctZGF0YS1zdG9yZS1tYXAYZGF0YWZsb3ctZGF0YS1tYXAtb2JqZWN0HHF1YW50bWUtaHlicmlkLXJ1bnRpbWUtZ3JvdXAXcXVhbnRtZS1ldmFsdWF0aW9uLXRhc2sqcXVhbnRtZS12YXJpYXRpb25hbC1xdWFudHVtLWFsZ29yaXRobS10YXNrGnF1YW50bWUtd2FybS1zdGFydGluZy10YXNrGXF1YW50bWUtb3B0aW1pemF0aW9uLXRhc2sdcXVhbnRtZS1vcmFjbGUtZXhwYW5zaW9uLXRhc2smcXVhbnRtZS1xdWFudHVtLWNpcmN1aXQtZXhlY3V0aW9uLXRhc2skcXVhbnRtZS1xdWFudHVtLWNpcmN1aXQtbG9hZGluZy10YXNrIHF1YW50bWUtcXVhbnR1bS1jb21wdXRhdGlvbi10YXNrLXF1YW50bWUtcXVhbnR1bS1oYXJkd2FyZS1zZWxlY3Rpb24tc3VicHJvY2VzcyVxdWFudG1lLXJlYWRvdXQtZXJyb3ItbWl0aWdhdGlvbi10YXNrC3RyYW5zYWN0aW9uDHNjcmV3LXdyZW5jaApjb25uZWN0aW9uEGNvbmRpdGlvbmFsLWZsb3cMZGVmYXVsdC1mbG93AAA=') format('truetype'); + font-family: "bpmn"; + src: url("data:application/octet-stream;base64,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") + format("woff"), + url("data:application/octet-stream;base64,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AADoDAAAAAwAAOgNAADoDQAAAA0AAOgOAADoDgAAAA4AAOgPAADoDwAAAA8AAOgRAADoEQAAABAAAOgSAADoEgAAABEAAOgTAADoEwAAABIAAOgUAADoFAAAABMAAOgVAADoFQAAABQAAOgWAADoFgAAABUAAOgXAADoFwAAABYAAOgYAADoGAAAABcAAOgZAADoGQAAABgAAOgaAADoGgAAABkAAOgbAADoGwAAABoAAOgcAADoHAAAABsAAOgdAADoHQAAABwAAOgeAADoHgAAAB0AAOgfAADoHwAAAB4AAOggAADoIAAAAB8AAOghAADoIQAAACAAAOgiAADoIgAAACEAAOgjAADoIwAAACIAAOgkAADoJAAAACMAAOglAADoJQAAACQAAOgmAADoJgAAACUAAOgnAADoJwAAACYAAOgoAADoKAAAACcAAOgpAADoKQAAACgAAOgqAADoKgAAACkAAOgrAADoKwAAACoAAOgsAADoLAAAACsAAOgtAADoLQAAACwAAOguAADoLgAAAC0AAOgvAADoLwAAAC4AAOgxAADoMQAAAC8AAOgyAADoMgAAADAAAOgzAADoMwAAADEAAOg0AADoNAAAADIAAOg1AADoNQAAADMAAOg2AADoNgAAADQAAOg3AADoNwAAADUAAOg4AADoOAAAADYAAOg5AADoOQAAADcAAOg6AADoOgAAADgAAOg7AADoOwAAADkAAOg8AADoPAAAADoAAOg9AADoPQAAADsAAOg+AADoPgAAADwAAOg/AADoPwAAAD0AAOhAAADoQAAAAD4AAOhBAADoQQAAAD8AAOhCAADoQgAAAEAAAOhDAADoQwAAAEEAAOhEAADoRAAAAEIAAOhFAADoRQAAAEMAAOhGAADoRgAAAEQAAOhHAADoRwAAAEUAAOhIAADoSAAAAEYAAOhJAADoSQAAAEcAAOhKAADoSgAAAEgAAOhLAADoSwAAAEkAAOhMAADoTAAAAEoAAOhNAADoTQAAAEsAAOhOAADoTgAAAEwAAOhPAADoTwAAAE0AAOhQAADoUAAAAE4AAOhRAADoUQAAAE8AAOhSAADoUgAAAFAAAOhTAADoUwAAAFEAAOhUAADoVAAAAFIAAOhVAADoVQAAAFMAAOhWAADoVgAAAFQAAOhXAADoVwAAAFUAAOhYAADoWAAAAFYAAOhZ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BAQJBAQEBQIBAQQEAxUDBAoHGQMKAgMDAwMDAwMCBgMCAwIDAggBnAUEBAEHChwLBxIECwQHBAQHBAgEEAgEAwICAQQEAwQFDAUNBAUEBQgFJgQJBAQEBAQEAQQDAQMBAwQECAQJBAICAQIGAwQF/kQEBQQGAgIBAgMDBQQBBA0JAQ0BBAUFBAEEBQUKBRkBBAEUBgUFBQsKCQUEBAMEAQIDAwQHEwUEBQUEBQUEJR4IBAQJDAMNAhIFBAUDARQEBAkIFiwFBQQFBQUEBRsEBgQBAQIDAwQEBAkDBgULBQUFBhQBHgUKBQUEAQQBBAUcDQQBBAUDAgECAgMECAH6/sgBODFaPrq6fQHyAQEBAQcCAgQDBAQECQQFBgMCCQIBAQEBAQECBQECAwMIBAUECQMDAwcBAQEBUwECAgQHCQkKAQkHBgcDBAMEAwQHCAcUBA0EBCADAwICAgIDAwgJBQYFBQQBBAEEBRwNBAEIBAEEBAQIAQMBAwEDBAgDAQMEAwEGBAcIAQQEAQMEAwEDDAMBAwEDAQgEBAQBBAQEAQQNFwUFBAEEAQkFCgUMBAUEBAMEAQIFBwweCAQJBBQHCAcEAwQDBAMHBgQDBwUEBAkEBAMEAQICBQwFCQQDBgIBAQEBAQICBwICAwICAgECAwMIBQkIAwUCAQIBAgECAgIJAgEBAScCAwUCBAECDwMCAQIJDAkBCQQDAwEDAwEDCwcHCQkEAwQEAQEEAwwDBAYDAwIDAwMDAwMCAwMCAwIDAgMCAwQDFgMDCAkJBwMEOwECAwMEDQULAgMDAwMDAwIDAwojAwcDBwQOCwMHAwgDAwIBAQMCBAgJBAIECQQEBAQEBQMBAwEDBAEDEAQDAQMIAwEKAQMDAQMDAQMDAQYECQEDAgMDqQEEBAMECAQECQUECQ0EGwUNCQQECQQBBCIEBQEEBAMDAgECAgMECAkFBBIECwQHBAQHBAgEHgQIBAcEBAcEBAkEBAQDBC0BBAgEBAQEHgUKBQUFBQkFJwUEBQkKBDIEBQQIBAYCAgYICgUZAQQBFAYFBQULBiYFBgULBgsGFgYJCAMDBBACBggICQYRBQYFBgsFIQYLBQUKBg8BBAEZBQoBBAYCAQEDAgQICQgBKQQFBQQFBQkFIgUJBQUFBQoFFwUEBwICOQECAwMIBwQHBBoDCwsHBgQDBxQPAwcBCAUEBQgDAwICAgIHAQMVAQMLAQcQBAQMBAQEBAQECQQmBQUIBAQDBKABBAMDBAkJCQYBDAEGDAYFAwMCAQQBBAECBQQCAgEBAQQBAQEGAQEBAgEBAgMHBQQFBAgDAwkCAQYDEAMGAwIDAgMCAwIDBgIDAwMDAwdKAQIBAwYMAwMFAwIBAQEEAwMECQUECAMDAgEBAQECCQICAgECAQIBAQECAgIBAQEEAQEBBQQDBQQFCAgDAgEnAQEDBgQJBQQEBAIFAwQJAQYJAwIBAgMCAQIDBAMKAggBAgECAgMBAgMDCAUECQQDAwMEAgECAQIBAgEQEAYCAwUJAwoBAgICDwIDBQYMFAECAQIBAgECBgIGCQQFBAQEAwMCAQEBAgICAQIBCAwDBAMCAQIDAgECEgEJBAMFBAQEBQkEAwMEAAAABQAA/5YDvQMjABYAMQA6AD4ARAAAASYOAxYXHgI3PgE3Njc2Jy4BJyYHNhcWFxYXFgYHBgcGBwYnJicmJyYnJjc+AhMGDwEXNRcRBycUFSclFBUnJicB9FimgUQBQD07obBRVIYnKQMGHh52Tl5rVlJQOjsXFxcrLERGWlhWWENDJyYDAyMecpQ/Nmwv0crKGI8BWSpDIgMiAUZ+obOmPj9JCR4delFTWFdVU4cmLzEBJiVDRFRPp0hKLzQQDxYWOTZQT1daTklwPv8AJk0hlI+PASiPYGRlZGVlZR4wFwAJAAD/ogO4AyAACwAXAFIAuwFEAaEB6AIyAp4AAAEVIxUzFTM1MzUjNQczFTMVIxUjNSM1MxMrAQcjByMHFQ8FFR8FMz8DMzczNzM3MxczFzMXMx8BMz8DNS8EIzUjJyMnFw8GFR8KMx8GFR8CMx8NMz8ENS8BNScjLwYjLwE1Iy8LIy8BIy8HIzUnNS8EIwUPASMPBBUPBBUHIwcVDwMjFQcVByMVDwQVByMPARUPAhUPAiMPAxUPAxUHFQ8KFQcVHwQ/BDM/BDU/JjUvAwEPBRUXFQcVBxUHFQcVBxUHFQcVBxUHFQcVBxUPBxUfBD8FMz8BMz8BNT8BMzczNzU3NTM1NzU3NTc1NzU3NTc1NzU3NSc1LwQFDwUVFxUXFRczHwUzHwEVFxUXFRcVMxcVFxUfBD8ENS8NNSc1JzUnNS8DAQ8XHwQ/BDM/AzM3MzczNzM3Mzc1NzM3NTczPwIzNzM1PwM1LwQFDwUVHwMVHwEzHwIzHwIVHwEzHwEVHwEzFzMXMxczHwIzFzMXFTMfBzMXFRczFTMXMz8ENS8EIycjJyMnIy8KIy8EIy8FIwGufX2MfX11Xn19Xn19MwcNBg0NBycNDgQDAwQBAgMDCQQFBgwGFwYLBgYGBikGBgYGBgUfBAUMAwMCAgYDCicGBwYNB+kFBAQDAwECAgIGAgIDBgUEAwgBDgIIAgYCBAECAgECAwIDBQQDBAMKBQYDCQQJBAQDBAIEBgEEAwIDAgMCAQIIAQUDBAIEAgIFAgcCAgECCQECAgMHAwIDAgECAwIGAgoF/fMFBwECAwIDAgMCAwIDBAECAgMCBAEEBAECAgICAgIBAgICBAICBAEBAQIDAgIKAQICAgEEAQIBAgECAQICAQQEAw4ECAQDAQECAwECAgIBAgECAQIBAgIDAgECCAIBAgICAQQCBAEGAgoCBgMKAwYDBQMCAQQGCAUCsgQFAwYCAQEBAQEBAQECAQMFAwUDBAMCBgMBAgMGBAkJBAQDAgEBAwIBAgMDAgEFAQwDAQIBAQEBAQEBAQQDBAQI/KYEBQMEBAEBAwIBAwIBBAEKAQQDAgMCAQIDAQIEBwkJBAQDBAIEAgMCBwQBBgECAQIIAQEBAQUICAKxBAQJBAUKBA8FBQsPBgULBSEGCxAIBAUCAgQDBA0JBgYUBQESBgcFAQUBBQEFARwBCwUBBQUBBQsPAQQBAwMCAQQDBAQI/d4EBAQDAgIBAgMEDQIBAgMCAQIDAwsCAQgDAwUBAgECAQIBAgMDAQIBAgECBAMGAw0DHQIDDQQDDQkFCAUCAQQDBAQDBAgDFAIRAwkFEQIGBQMFBQUCAQ8CBQICAQQDBBEEBAUCIX2MfX2MfRd9Xn1+XQGTAQIGAQIEAgMECAkFBAQDBAECAgIEAgEBAQEBBQEGBAMJBQgIAgQGAQEBTwEBAwMEBAQFCQQGAQIBBgMEAggOAwgDBgMEAQICAwQDBAMHBwMIAxIKCAIEAgIDAwgKBwcBDAgEBAQEBAQECwEHAwYCBgIDBQMHAwIDCQMCAgcCAgICAQEBAgIEAgUMAQQCAgICAgECAgICAgEEAgECAgMEAQQBBAECAwIDAgECAwIBAgYCAQIGAwMCBQIBAg8DAgECAQIDBgMDAwMDAwMDAQQJBQgDAwMBBAMDBAYFAwIF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OBoJFhhQYwkUHQkJBAgECQ0EWVkSJg4/BA4NCgUHBgICAwgIBAEIBCYECAMEBAQHBAgDGgMIBwcDCQUEBAQDBAEEAwcBAwUEBAQEBAkEJgQJBQQEBQ2xBQMGBwEDCAMBFQMBAwMBCQEMAQkJAwMCAQIDAwgEAQQCAgIDAwgEBQQJAwgDAgQDAgMCAwIDAgMDAwIDAwMDAwMDAwYDIwIDBAQCAgUECAUBhAQEBAMDAwEGAwMCAwIDAgMCAwcWAQQBBAEGAwICBAQDBAUJBAQEAwQBAgICAQIBAgECAgIBAQEIAQEBAgIEAQIHAwIBAgMCAQIGAwkDAwMECf3HBAgDAwICAQIBAQEBAQECAQgCAQEBAwMEBAQFCQQDAwQBAgUDAgEBAQEBAQIBAQIDAwkEAo0FBAQDBAICBwIDBQIEAQIDDAoDBAIEAQEEAwQECQgFBwMPAgEGBQgBAQEGAQIBAgECAgIJAQICAwQIBf2uBAQEAwICAgUGDAYBDAEGBwMBAwMBBwcDAQcHAQMBAwQIBAQMBAQEBAkEBAQFAgEBBAQDFQMECgcZAwoCAwMDAwMDAwIGAwIDAgMCCA4BpQQEAQcKHAsHEgQLBAcEBAcECAQQCAQDAgIBBAQDBAURDQQFBAUIBSYECQQEBAQEBAEEAwEDAQMEBAgECQQCAgECBgMEBQSnBQYGCwUMBQUGJgYKCAQCAwEBAgYECQQrBQkFBQUFCgUeBQoFBQUFCgQYCQUEAwQEAQEEAwQHJgUGBQsGCwbvBAUDBAQCAggCDAcIBwQDBAMEAwcGBw8CCQIDEQQDBAkJBAQDBAECAwIBAgMCAQISAQkEBgEDBAMBAwgEAwEDAQMBCAQEBAEECAEECgj99AQIBQQBBAQEDAQEBAgDAQMEAwQDBAkBDwIBAgMDBQIBBAMDAQIDAwgECQUHBQUQBgIGAg8HBgcDBAMEAwQHCAQEBwIBAgIDBAgEAq8ECAYCAQEBAQECCAIBAgIEARYBAQQDDAUJBAYFBAMKAggBAgECAgIHAQEBAQEEAwQIBPymBAQIBAIBAQECBgICAgEEAQYCCgMEAQMGBAUJBAQEAwQCARIBAgECAQICAgcCAQEBAQIHAwUElAUEBgICAQIDAwUEAQQNCQENAQQFBQQBBAUFCgUZAQQBFAYFBQULCgkFBAQDBAECAwMEBxMFBAUFBAUFBCUeCAQECQwDDQ0CGgQFAwEUBAQJCBYsBQUEBQUFBAUbBAYEAQECAwMEBAQJAwYFCwUFBQYUAR4FCgUFBAEEAQQFHA0EAQQFAwIBAgIDBAgEAjQBSzs5gFkeFRVtQD5xGhUnASI9SUw/KAcaOE1RISUsMhIjNgEPEQMDAQYZIkQI7QECAgUMBQkEAwYCAQEBAQECAgcCAgMCAgIBAgMDCAUJCAMFAgECAQIBAgICCQIBAQEnAQEDBQIEAQIPAwIBAgkMCQEJBAMDAQMDAQMLBwcJCQQDBAQBAQQDDAMEBgMDAgMDAwMDAwIDAwIDAgMCAwIDBAMWAwMICQkHAwQ8AQEDAwQNBQsCAwMDAwMDAgMDCiMDBwMHBA4LAwcDCAMDAgEBAwIECAkEAgQJBAQEBAQFAwEDAQMEAQMQBAMBAwgDAQoBAwMBAwMBAwMBBgQJAQMCAwMCqwEEBAMECAQECQUECQ0EGwUNCQQECQQBBCIEBQEEBAMDAgECAgMECAkFBBIECwQHBAQHBAgEHgQIBAcEBAcEBAkEBAQDBAF3AQIDAwgHBAcEGgMLCwcGBAMHFA8DBwEIBQQFCAMDAgICAgcBAxUBAwsBBxAEBAwEBAQEBAQJBCYFBQgEBAMEoQICAwMECQkJBgEMAQYMBgUDAwIBBAEEAQIFBAICAQEBBAEBAQYBAQECAQECAwcFBAUECAMDCQIBBgMQAwYDAgMCAwIDAgMGAgMDAwMDBwNNAQIBAwYMAwMFAwIBAQEEAwMECQUECAMDAgEBAQECCQICAgECAQIBAQECAgIBAQEEAQEBBQQDBQQFCAgDAgECzgEBAQEHAgIEAwQEBAkEBQYDAgkCAQEBAQEBAgUBAgMDCAQFBAkDAwMHAQEBAVMBAgIEBwkJCgEJBwYHAwQDBAMEBwgHFAQNBAQgAwMCAgICAwMICQUGBQUEAQQBBAUcDQQBCAQBBAQECAEDAQMBAwQIAwEDBAMBBgQHAgoBBAQBAwQDAQMMAwEDAQMBCAQEBAEEBAQBBA0XBQUEAQQBCQUKBQwEBQQEAwQBAgUHDB4IBAkEFAcIBwQDBAMEAwcGBAMHBQQECQQEAwT+xQEECAQEBAQeBQoFBQUFCQUnBQQFCQoEMgQFBAgEBgICBggKBRkBBAEUBgUFBQsGJgUGBQsGCwYWBgkIAwMEEAEBBggICQYRBQYF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BwMEAwQDBAcIBAQHAgECAgMECAEEEg0KBQcGAgIDCAgEAQgEJgQIAwQEBAcECAMaAwgHBwMJBQQEBAMEAQQDBwEDBQQEBAQECQQmBAkFBAQFDa0ECAYHAQMIAwEVAwEDAwEJAQwBCQkDAwIBAgMDCAQBBAICAgMDCAQFBAkDCAMCBAMCAwIDAgMCAwMDAgMDAwMDAwMDBgMjAgMEBAICBQQIAYQFCAQDAwMBBgMDAgMCAwIDAgMHFgEEAQQBBgMCAgQEAwQFCQQEBAMEAQICAgECAQIBAgICAQEBCAEBAQICBAECBwMCAQIDAgECBgMJAwMDBP3DBQQIAwMCAgECAQEBAQEBAgEIAgEBAQMDBAQEBQkEAwMEAQIFAwIBAQEBAQECAQECAwMJAu4FBAgGAgEBAQEBAggCAQICBAEWAQEEAwwFCQQGBQQDCgIIAQIBAgICBwEBAQEBBAMECPynBQgIBAIBAQECBgICAgEEAQYCCgMEAQMGBAUJBAQEAwQCARIBAgECAQICAgcCAQEBAQIHAwUC8AQFBAQDBAICBwIDBQIEAQIDDAoDBAIEAQEEAwQECQgFBwMPAgEGBQgBAQEGAQIBAgECAgIJAQICAwQI/a0ECAQDAgICBQYMBgEMAQYHAwEDAwEHBwMBBwcBAwEDBAgEBAwEBAQECQQEBAUCAQEEBAMVAwQKBxkDCgIDAwMDAwMDAgYDAgMCAwIIAZwFBAQBBwocCwcSBAsEBwQEBwQIBBAIBAMCAgEEBAMEBQwFDQQFBAUIBSYECQQEBAQEBAEEAwEDAQMEBAgECQQCAgECBgMEBf5EBAUEBgICAQIDAwUEAQQNCQENAQQFBQQBBAUFCgUZAQQBFAYFBQULCgkFBAQDBAECAwMEBxMFBAUFBAUFBCUeCAQECQwDDQISBQQFAwEUBAQJCBYsBQUEBQUFBAUbBAYEAQECAwMEBAQJAwYFCwUFBQYUAR4FCgUFBAEEAQQFHA0EAQQFAwIBAgIDBAgCIX2MfX2MfRd9Xn1+XQGVAQEBAQcCAgQDBAQECQQFBgMCCQIBAQEBAQECBQECAwMIBAUECQMDAwcBAQEBUwECAgQHCQkKAQkHBgcDBAMEAwQHCAcUBA0EBCADAwICAgIDAwgJBQYFBQQBBAEEBRwNBAEIBAEEBAQIAQMBAwEDBAgDAQMEAwEGBAcIAQQEAQMEAwEDDAMBAwEDAQgEBAQBBAQEAQQNFwUFBAEEAQkFCgUMBAUEBAMEAQIFBwweCAQJBBQHCAcEAwQDBAMHBgQDBwUEBAkEBAMEAQICBQwFCQQDBgIBAQEBAQICBwICAwICAgECAwMIBQkIAwUCAQIBAgECAgIJAgEBAScCAwUCBAECDwMCAQIJDAkBCQQDAwEDAwEDCwcHCQkEAwQEAQEEAwwDBAYDAwIDAwMDAwMCAwMCAwIDAgMCAwQDFgMDCAkJBwMEOwECAwMEDQULAgMDAwMDAwIDAwojAwcDBwQOCwMHAwgDAwIBAQMCBAgJBAIECQQEBAQEBQMBAwEDBAEDEAQDAQMIAwEKAQMDAQMDAQMDAQYECQEDAgMDqQEEBAMECAQECQUECQ0EGwUNCQQECQQBBCIEBQEEBAMDAgECAgMECAkFBBIECwQHBAQHBAgEHgQIBAcEBAcEBAkEBAQDBC0BBAgEBAQEHgUKBQUFBQkFJwUEBQkKBDIEBQQIBAYCAgYICgUZAQQBFAYFBQULBiYFBgULBgsGFgYJCAMDBBACBggICQYRBQYFBgsFIQYLBQUKBg8BBAEZBQoBBAYCAQEDAgQICQgBKQQFBQQFBQkFIgUJBQUFBQoFFwUEBwICOQECAwMIBwQHBBoDCwsHBgQDBxQPAwcBCAUEBQgDAwICAgIHAQMVAQMLAQcQBAQMBAQEBAQECQQmBQUIBAQDBKABBAMDBAkJCQYBDAEGDAYFAwMCAQQBBAECBQQCAgEBAQQBAQEGAQEBAgEBAgMHBQQFBAgDAwkCAQYDEAMGAwIDAgMCAwIDBgIDAwMDAwdKAQIBAwYMAwMFAwIBAQEEAwMECQUECAMDAgEBAQECCQICAgECAQIBAQECAgIBAQEEAQEBBQQDBQQFCAgDAgEnAQEDBgQJBQQEBAIFAwQJAQYJAwIBAgMCAQIDBAMKAggBAgECAgMBAgMDCAUECQQDAwMEAgECAQIBAgEQEAYCAwUJAwoBAgICDwIDBQYMFAECAQIBAgECBgIGCQQFBAQEAwMCAQEBAgICAQIBCAwDBAMCAQIDAgECEgEJBAMFBAQEBQkEAwMEAAAAAAgAAP+aA7gDIgAUACkALgAyADYAOgA+AEIAAAEiBwYHBhQXFhcWMjc2NzY0JyYnJgcyFxYXFhQHBgcGIicmJyY0NzY3NgcVESERBTMRIxMVMzUHFTM1BxUzNQcVMzUB9HpqZjw+PjxmavRqZjw+PjxmanpuXls1Nzc1W17cXls1Nzc1W14pAS7+6v7+GsrKysrKysoDIj48Zmr0amY8Pj48Zmr0amY8PjE3NVte3F5bNTc3NVte3F5bNTfVDP6QAXwY/rQBJxkZShgYVRgYTRgYAAAACgAA/6IDuAMgAA8AHwAxAGwA1QFeAbsCAgJMArgAAAEmBgcGHgI3PgE3NiYnJgc2HgIOAy4CNjc+ARcGDwEOARYXFj4BOwE1IzY3JgMrAQcjByMHFQ8FFR8FMz8DMzczNzM3MxczFzMXMx8BMz8DNS8EIzUjJyMnFw8GFR8KMx8GFR8CMx8NMz8ENS8BNScjLwYjLwE1Iy8LIy8BIy8HIzUnNS8EIwUPASMPBBUPBBUHIwcVDwMjFQcVByMVDwQVByMPARUPAhUPAiMPAxUPAxUHFQ8KFQcVHwQ/BDM/BDU/JjUvAwEPBRUXFQcVBxUHFQcVBxUHFQcVBxUHFQcVBxUPBxUfBD8FMz8BMz8BNT8BMzczNzU3NTM1NzU3NTc1NzU3NTc1NzU3NSc1LwQFDwUVFxUXFRczHwUzHwEVFxUXFRcVMxcVFxUfBD8ENS8NNSc1JzUnNS8DAQ8XHwQ/BDM/AzM3MzczNzM3Mzc1NzM3NTczPwIzNzM1PwM1LwQFDwUVHwMVHwEzHwIzHwIVHwEzHwEVHwEzFzMXMxczHwIzFzMXFTMfBzMXFRczFTMXMz8ENS8EIycjJyMnIy8KIy8EIy8FIwH0QG8XGRhWfjo9UQEERDkrMiZIMxMOK0FQSzgaCRYYUGMJFB0JCQQIBAkNBFlZEiYOOwcNBg0NBycNDgQDAwQBAgMDCQQFBgwGFwYLBgYGBikGBgYGBgUfBAUMAwMCAgYDCicGBwYNB+kFBAQDAwECAgIGAgIDBgUEAwgBDgIIAgYCBAECAgECAwIDBQQDBAMKBQYDCQQJBAQDBAIEBgEEAwIDAgMCAQIIAQUDBAIEAgIFAgcCAgECCQECAgMHAwIDAgECAwIGAgoF/fMFBwECAwIDAgMCAwIDBAECAgMCBAEEBAECAgICAgIBAgICBAICBAEBAQIDAgIKAQICAgEEAQIBAgECAQICAQQEAw4ECAQDAQECAwECAgIBAgECAQIBAgIDAgECCAIBAgICAQQCBAEGAgoCBgMKAwYDBQMCAQQGCAUCsgQFAwYCAQEBAQEBAQECAQMFAwUDBAMCBgMBAgMGBAkJBAQDAgEBAwIBAgMDAgEFAQwDAQIBAQEBAQEBAQQDBAQI/KYEBQMEBAEBAwIBAwIBBAEKAQQDAgMCAQIDAQIEBwkJBAQDBAIEAgMCBwQBBgECAQIIAQEBAQUICAKxBAQJBAUKBA8FBQsPBgULBSEGCxAIBAUCAgQDBA0JBgYUBQESBgcFAQUBBQEFARwBCwUBBQUBBQsPAQQBAwMCAQQDBAQI/d4EBAQDAgIBAgMEDQIBAgMCAQIDAwsCAQgDAwUBAgECAQIBAgMDAQIBAgECBAMGAw0DHQIDDQQDDQkFCAUCAQQDBAQDBAgDFAIRAwkFEQIGBQMFBQUCAQ8CBQICAQQDBBEEBAUCNAFLOzmAWR4VFW1APnEaFScBIj1JTD8oBxo4TVEhJSwyEiM2AQ8RAwMBBhkiRAgBSAECBgECBAIDBAgJBQQEAwQBAgICBAIBAQEBAQUBBgQDCQUICAIEBgEBAU8BAQMDBAQEBQkEBgECAQYDBAIIDgMIAwYDBAECAgMEAwQDBwcDCAMSCggCBAICAwMICgcHAQwIBAQEBAQEBAsBBwMGAgYCAwUDBwMCAwkDAgIHAgICAgEBAQICBAIFDAEEAgICAgIBAgICAgIBBAIBAgIDBAEEAQQBAgMCAwIBAgMCAQIGAgECBgMDAgUCAQIPAwIBAgECAwYDAwMDAwMDAwEECQUIAwMDAQQDAwQGBQMCBQECAwIDAgMCAwMCBQIDAgwCAwIDAgMEAwQDBgMKAwYCCgEGAgUEBAQJCQYEAf7GAQICCAQEBAQzAwkDBgIGAwMDAwMDBgIDAwkCDgMIAQ0FCwUGCgcEBQkEBgICAgIDAwMDBgYGBQEFBw8mAwoDAwcDAwQDAwQDAwQDBwMOAywFCQgEAgMCEwECAwMIBBsGBxMHDRMGBw0GHw0FAQUBBQEFAQUBBQEBBAQEAgICAwMJCQgGBgUGEAsGEQUGBgUvBgUGBgwGBwUHBgL+sgECBgQEBgQJBAMFCQIDBAMMAQQDBAMHCQkIAwMDAgIBBgIGAwIDAgMDDwYBAwMBAwQHDAQBAwQEBAkIBAMCAgIBAwIEBAkEBQQEAwEKAgICAgICAQEHAgUBAQEDAgIBAgECAQEBAQECAgIE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AwMEAQIDAwkEBQYMBhcGCwYGBgYpBgYGBgYFHwQFDAMDAgIGAwonBgcGDQfpBQQEAwMBAgICBgICAwYFBAMIAQ4CCAIGAgQBAgIBAgMCAwUEAwQDCgUGAwkECQQEAwQCBAYBBAMCAwIDAgECCAEFAwQCBAICBQIHAgIBAgkBAgIDBwMCAwIBAgMCBgIKBf3zBQcBAgMCAwIDAgMCAwQBAgIDAgQBBAQBAgICAgICAQICAgQCAgQBAQECAwICCgECAgIBBAECAQIBAgECAgEEBAMOBAgEAwEBAgMBAgICAQIBAgECAQICAwIBAggCAQICAgEEAgQBBgIKAgYDCgMGAwUDAgEEBggFArIEBQMGAgEBAQEBAQEBAgEDBQMFAwQDAgYDAQIDBgQJCQQEAwIBAQMCAQIDAwIBBQEMAwECAQEBAQEBAQEEAwQECPymBAUDBAQBAQMCAQMCAQQBCgEEAwIDAgECAwECBAcJCQQEAwQCBAIDAgcEAQYBAgECCAEBAQEFCAgCsQQECQQFCgQPBQULDwYFCwUhBgsQCAQFAgIEAwQNCQYGFAUBEgYHBQEFAQUBBQEcAQsFAQUFAQULDwEEAQMDAgEEAwQECP3eBAQEAwICAQIDBA0CAQIDAgECAwMLAgEIAwMFAQIBAgECAQIDAwECAQIBAgQDBgMNAx0CAw0EAw0JBQgFAgEEAwQEAwQIAxQCEQMJBRECBgUDBQUFAgEPAgUCAgEEAwQRBAQFAhwM/pABfBj+tAEnGRlKGBhVGBhNGBgCLQECBgECBAIDBAgJBQQEAwQBAgICBAIBAQEBAQUBBgQDCQUICAIEBgEBAU8BAQMDBAQEBQkEBgECAQYDBAIIDgMIAwYDBAECAgMEAwQDBwcDCAMSCggCBAICAwMICgcHAQwIBAQEBAQEBAsBBwMGAgYCAwUDBwMCAwkDAgIHAgICAgEBAQICBAIFDAEEAgICAgIBAgICAgIBBAIBAgIDBAEEAQQBAgMCAwIBAgMCAQIGAgECBgMDAgUCAQIPAwIBAgECAwYDAwMDAwMDAwEECQUIAwMDAQQDAwQGBQMCBQECAwIDAgMCAwMCBQIDAgwCAwIDAgMEAwQDBgMKAwYCCgEGAgUEBAQJCQYEAf7GAQICCAQEBAQzAwkDBgIGAwMDAwMDBgIDAwkCDgMIAQ0FCwUGCgcEBQkEBgIC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CAQDAwMBBgMDAgMCAwIDAgMHFgEEAQQBBgMCAgQEAwQFCQQEBAMEAQICAgECAQIBAgICAQEBCAEBAQICBAECBwMCAQIDAgECBgMJAwMDBP3DBQQIAwMCAgECAQEBAQEBAgEIAgEBAQMDBAQEBQkEAwMEAQIFAwIBAQEBAQECAQECAwMJAu4FBAgGAgEBAQEBAggCAQICBAEWAQEEAwwFCQQGBQQDCgIIAQIBAgICBwEBAQEBBAMECPynBQgIBAIBAQECBgICAgEEAQYCCgMEAQMGBAUJBAQEAwQCARIBAgECAQICAgcCAQEBAQIHAwUC8AQFBAQDBAICBwIDBQIEAQIDDAoDBAIEAQEEAwQECQgFBwMPAgEGBQgBAQEGAQIBAgECAgIJAQICAwQI/a0ECAQDAgICBQYMBgEMAQYHAwEDAwEHBwMBBwcBAwEDBAgEBAwEBAQECQQEBAUCAQEEBAMVAwQKBxkDCgIDAwMDAwMDAgYDAgMCAwIIAZwFBAQBBwocCwcSBAsEBwQEBwQIBBAIBAMCAgEEBAMEBQwFDQQFBAUIBSYECQQEBAQEBAEEAwEDAQMEBAgECQQCAgECBgMEBf5EBAUEBgICAQIDAwUEAQQNCQENAQQFBQQBBAUFCgUZAQQBFAYFBQULCgkFBAQDBAECAwMEBxMFBAUFBAUFBCUeCAQECQwDDQISBQQFAwEUBAQJCBYsBQUEBQUFBAUbBAYEAQECAwMEBAQJAwYFCwUFBQYUAR4FCgUFBAEEAQQFHA0EAQQFAwIBAgIDBAgCHAz+kAF8GP60AScZGUoYGFUYGE0YGAIvAQEBAQcCAgQDBAQECQQFBgMCCQIBAQEBAQECBQECAwMIBAUECQMDAwcBAQEBUwECAgQHCQkKAQkHBgcDBAMEAwQHCAcUBA0EBCADAwICAgIDAwgJBQYFBQQBBAEEBRwNBAEIBAEEBAQIAQMBAwEDBAgDAQMEAwEGBAcIAQQEAQMEAwEDDAMBAwEDAQgEBAQBBAQEAQQNFwUFBAEEAQkFCgUMBAUEBAMEAQIFBwweCAQJBBQHCAcEAwQDBAMHBgQDBwUEBAkEBAMEAQICBQwFCQQDBgIBAQEBAQICBwICAwICAgECAwMIBQkIAwUCAQIBAgECAgIJAgEBAScCAwUCBAECDwMCAQIJDAkBCQQDAwEDAwEDCwcHCQkE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AQICAQQFAwECAgIHBAYDBAMCBQcBAQIDBAEFAQICEgEBAQEBAQEIAQYBVgEFAwIDAgEBAQMCBwMBAQMCAQgGAQIEAgECAgcGBAQDAwIZBgQEAQIBCgECAwIDAQEBAQIDBQIFAgQCAQEDAQQCAgEBAQEBAQMEBAECAQMFBQEBAQECEQIDAQECAQEMAQEcAg4CAwgGAQIBDgoCBAIBBQECCAkBBAICAQQDAQYDCQ4CAQIEAgYBCgMHBAMBAQQBAgIEBwQNAQgBBgQQBAICBAQHBQEBBQQJAwICAgMFAQECAQEECQQGAQEBAQIEAgICAQQGAwEEHSEBAwUBAQMbFwoB0wIBAwEBCgIBAQEHBwgBAQURCwcGAQEBAQMCAgQFBwkCBwQDBwgMCgcBAgEDBQICBAMCAQEDBgYFBAoFDw4BAQIEAQEGAQIECAMCAQMBAgYBAgIBAQEBCwYFAQIEBgYCAgUBDQkGAQEBCgMCBQkBAgUEBAYDAQMFAgMBAQIEAQQGBQcHAQEBAQMIAwcBAwUNAwoDAgQBAQEDAgECAQYLAwEBAgECCgQD6gMJCgICAgECAgQCCAYDCAMCBQEDBQMBAwICAQMBAQQDAQECAwIBBAkEAQEHAwMDAgEDAQECAQIBBQEBChEEAwIBBAIFBAcDAQgFAwYEBwIFBQMCAQECBQsFEgUCDwEGBwEBAwMCAwEEBAEBAQMFBwIEAwICAQIBAQEDAQMCAQIBAwMBBAEEAQEBAgIDBQQCBAEGBwoJCwcBAQcEBAUOAgUBAwQFBAEDDAQFAQICAgECAQECBQIEAgUMBAEBAQQBAQEEAwEBAQIJBwUBBgEBAQMJCwEFBwYEAwEBAQMBBwYJCwMHAQEDAwIBAgECBAcKAwEBAgQBBAEEAQQCAgEBAwQBBAMCBAMBEQIDBAsOBAMBAgEBAwEBBAECAgEBAQEBAwMDAQMDAgYDAwkLAwIBAgEDAwYIAQMCBAECAgIEAQMJCQMFBAoBAwQBAQECBAQBBAMBAgMBBwIDBAEHAgECBQMEBQQBBwMDAwEFAQICBQYHBwIBAgIBAQMBBA0EBxAMBwQBAgMBAwIJAQIBAgEFAQQCAgEBAgIBAQIFBAgPAQYJAQIBAQMBAQcLAwEIAgIBAwIDAQIGAwMEAQECAwIIBA8HAgQBAgcFAwICAgEBAgQFAgMBAgUCBAMBmAYGCAoCBgcKCgcCAQQJBQ8LAwQEBgQFAgEBAgoHCAgBAQEOBwcFAQECAQIDAQEBAgEKAwECBAEBAgIBAQECAQEFCgICAQQBAQEEAwYIAQICAgMHBgMDDQcBBQECAQQBCAQLBwMBAQEBBQYHBAMEAwMBBAIJBAQHBwEBAwENAgIDAQIEBAgFCQUCAgMBAgIDAQULBAIBAQsFBgICAQEGAQIBAQEDBAIEAQIEBwQCAgIBAQMCBA8CJyEBBAEBAQICCBsBAg4BAQEDAgECAQQBAQEDAQE/AQEBAgMHAwQBAQMFAgECAQEBAwQEAwECBAIFAQMCAQkBBQMBAwEBAQEBAgECAgQIAwYBAQUBAwMOBwcGAwEBAgMFAQIFAgEBAQEDBAMCAQIBCAUDAgEBAQECAgMMBAQMCQEHAgEJAwECAQICAgEDAwEBAQEHCAwBAQQIAgUBAwMCAQICAgQBAgEBAQIBAgICAgMBAgEBBAQBAwEGBAoHAgEBAgUDAgECAgECAwIEBgMDAgIBAgEBAQEFBAECAQIFAwMDBgQEAQEEAQMDCAUJBQEBBwEEAwEEAQEBAgEBAQIHcX4BAUkCATMBAQEGBwEBAgIBAgEBAQEDAgEBAhPOAQGZATALAQG1AZoCAawCARYB7wICFbYcAQEBAQEVIsbJAgIFAgEBAQEBBQEBoboBBQUBBAQCBgYIAwcFAQQhASUlBQUBBAEHAwchARUpYFMCBAEGAwMGBgEBAQIEBQIBAQICAwUBAgYFAQECAQMBAwEeARMBCAEBDQOlAQEBBQMEAQEBAwMBBAQBAQICAwQHAggCAQICBwUDBQMBAgICAxQBAgQCAQEBBAcDAQEBAwUDAQECAQUBAQMBAgECAwQBAgMIAgECAQQDAQEEAQYEAQgCAwcDAR4BDAUOAQEvAQEBBQMKAgYCBAcCBAQEBhAFBAQKAwMBBQMBAQIDAQEBAgEFAQcDAQQCAgEBAQEBAgQBAgYICgcBAgQDAwEBAwECAgILAQQBAgUEAQMBAQECAQIDAwECAQEEAgMCAwICAQECBgQDBwQBBggEBwgDAgEBBAEBAQICAQEBAwMCBAEICAYGBAYEAggGAQkDAQIEAQUEDAMBAwYIAwYDAgQBBAECAQUECgMBBAEBAgEGAgICAgMCAQECAQEBAgYFAgEEAQIEAQIGAgECAwQCAQEBAQECAwICBAMCAgMCAgoBBQYCAQIBAgEDAgMEBQYCAgEBBgICAQIECQUBCAYCAwIDAQECAgQJAwEEAQEGCgQJEAgCCQMEAgEBOgEBAQIQAgEBTgEEAjYrAQ8BAjIkDQEBKAEBDhAGJTUIAgIDAgEBAgIDAgEBAQQEDhYKAQYDAQECAQIEAwQCAQgIAQEFBQMCBAUBAwECAwEBAQEIBQMBAgcBAgYCAgMBAgIDAQEBAQMECQoBAgQBAQEBAg0BBAgEAwEBBF0FBQcGAwICAgEBBAUHAgkFAQIEAQMBAgIDCAYFAQMEAQEBBwQIAQEBAgEGBQEBAQYEBgIDBQgDAwEBAQMDCxMHAgEBBgUEAQMBAQECAQIDAQUEAQEoAR8UAQMCLwEBBgIBBAECAgEDAQMCAwEHAgIBAQIBAQcEAQIBAQIBAQQBCQUBBgIEAgITBgcBAwUBBAEFAwQJAQEBAgcBAQECAgEBAgIDAQECAwEBAQEBBwQFAQoMAwELAQIBAwMHAgECAQEBFAIBASYBARMCAQEbAQEBUQEIBgICAQEDAwQCBQUBAwIBAQEBAQMDBAEFAQIDAgEBAQQICAQEAQEBAQIIAgECBAMEBQMFAwMBAwICAQECAQEDAgEEAwMEAgQEAQEBAwEPCgEBPAEIAgYCBAEBBRIEBAEEAgMBAQEBAgECAQ0CAQEFAQIEAgkLAgECAQYEAQMIAwQBAQEHBQENCAgCAwECCwIBCAIDAQQPBAEDAQQFBAQDAwIBBAcFAQIBBQUDAQIDCAcCAQEBAwMCAQQCBAEHAgEDAxgNFAUBAQEBAgsBAQUDBQIGAQEUCwUNBQUMChoNAQUFAg4GAwEJBQYRCAwPAwMCAwwUAwQEAwICAgEEAQoCASEBpAICAQEFDR4EIB4OAQUCAgkFFSoBBQMWFwYBAgMEAwIBBQEFARABAwUCAgEBAQoYByAcDwUQCQYBAQcGIx4BCAYcEQkCAQICBAMBAQERBAoCBAMEAgEDAgUDBAEIAxEDAxMCAgYPAgEIBhoFBAQIFQEBAQUCBAMDAgMGAQQBAQQBCwQFAQMDGwQBBQEZDwUJAwQBFAMCBQQBBAIEAQICBQQDFw0DBBEFCgUQDQcCBAEECQQBAQcBDAcBAgEFAQEFBgyTDxIHCAYGDAsHBgQBAgcmBAMCBAIGEBMBBAIEBhQPCAcDFwwBAQMCBgUIDAQLBhEOAQFWDxAHBQcBCBERBAUBAgYBAgIREwkKCAcQCwgFBAECAQQQBAkCAgYFBQsCBwQBDA4JEAcBCAUQQQECF00BAQ8PExYGCwcHCwYGCxQfMTIZAgEDAQEaEg4KEQwIGAMJBAMDAgYOBgMBCQEBEgoIAQEBFwcKDggOExcCJwQJAwgJEQQBFQEcEhEKCgQHBgoHDQQGEAcDAgQGDQ8RBg8GAyHXAQQIAwEHDgICAQMPPwsDATMBAwQFCQYEAQIJAQ8lBgEBBQEECQcFBQIVBAIBAwEBIVQMAgIEBuYBEQsSAgEFAQUGAwgFBgMFBAcDBxQIDgIXFxYVCAEFBgwZEQgQBAkFBAYBAwEFAwUCEA8NCRUHBwkMBwkFDREQAwMGAgocJg4QCwgDAQEBFy0OEjsRBQYBBQQBAwEBAgUFAREJDgEKCRYQGgYJAQMHCQ0KGgwMCQYCAwUJBAIIAQgNDQUKAgYIBhMHCg4CDgcPEgMEBQMEAwwNERQYHg4IAxcVGg4TAgESChIGBQYECQUy2QGTBQMCBA8HDgcCBAUCBAIIBwsJEwUEAZoKDwMFCxk3Q1ViQjsiFAsMDgKnDQkWJg0XEBoaERYNJhYJDQ0JEhIUDRkZDhMJERMNnA0MKyAjK/2SKyMgLBgYLCAjKwJuKyMgFRYMDSwMDCgbFhX9khQYGSoLDAwLKhkYFAJuFRYbKAwM4A0JIBkTECAIAwMIIBATGSAJDQ0RDBENHwcDAwcfDREMEQ0NCQMJDQ0JAwkNcw0JIBcTDhMLBhIDBgECAQMBBQgcDg8GBgMCCQ1pLCxiDQkCCQ0NCQIJDVkG/q0GLB0hLi56fC8SLHFuVCAZJgGcASQiLnx7LwYBVwYiERERAR4c/sEscHAqICJIAQIEBgIIAQIBAQIGBgQBAgEEAQQEAwQCBgMDCwMCAQEDAQECAQEBBwUCAgEDAgMCAQMHBQIDAQMBBAQEAgEBAgECAQEDCAEBAgIBAQMBAwEBCAIDAQIiAQEDAQMGBQEHBAEDAgIFAQUEAwICBgQJAgEBAgICAgEDAgMEBgUKCgQDCQUBAQEDBAoDAgIFBwIEAwEEAwIEAQIDAwQFAQUIBQIFAgEHAgkHAgYCBgYBBQIDAwMLCwYBBAsBAhQCBQUCBQQBBggDAgEBAR0BBwoGCAIEBQQUCgMFAwMDAQoGBwYCCQQDAgIDAQECAQIGAQECBAEDAQEBAQMFAgUCBAICAQICAwIBAgYGAgEEBgUEAQQEBQECAQIDBAEFDAcEAwYBAQEBAwYCAQICAwEDAgQBAgIDAwMDAQMBAwEBAQMBBwEDBAEEAgIFAQEBAgMBAQQBAQIBAQEGAgICAQECAgIGDwEEAQIBAQMBAQICAQEBAQEEAQFOgwQDAQQDBQIBAQEGDhUKAQQFAgMJAQQCAQIDAwMLCQEBAgMEAgIBBQEFDQMBAQEFBAIBAQIEAQQCAwECBAMDAQIEAQEEBwcBAgECBwMBEQoGAwEBAQICAQIIAwQHBAQIAQQFDAEBBAEGAwEBAgEBAQoFAQMFAQIGAgMGARILBQYBAQECAQECAwQCBAEEAwMCCAICBAICBAIBBAMBAQEEAgECAgEEAgkBAQQKAQExAwIFAgsDBAUBCwMKAwEIAgIBAgMJAQMCAgQCAwEBAgITAwQEBgMCAgMBAQIBAQUBAQYBAQEDAgEDBgQCAwIDAQMECgECBQgCAQUEBAUBAgIBAQEBAQIBAQEDBwEBAQECAQQFAT4BAgQFAQEDAgIDAQUEAwkCAwEBBwgGAgYDAQMCBQMIBAcEAgIBAgEbAwEHAwYCCgoBAgIBAgIGBgICAQIFAwEBAgQEAwQCAQEBAQEFCAEBAwIEAQIBAgUGAw8EAQUBAgIMETUCBwMGCwIBBQcSBAEBAQIEAgIEAwYCBAEFBwYCBQ8KAwIBAQQICwYCBQEBAgEDCgEBBAIIFQgCBA4PBgYCBQIGAQcEBgEBAQEEBwMCAwEBAgMHBgYBAwMBAQEBAgEBAQICAgIBAwMCAgwCAQYCAQEBAwYBMQELAZ0DBAUCAgEBAQUBAQUCAQQDCggCAQYBAQEDAQIBAwkBBgEBAQQLCgIBAQEBAgMCAQECAwIHCQIQBwsDAQMCAwECAwICCAwLBggEAggBBwQEBAIBAQkIAwMCDAcECgUHBQECAgIIAQEBAQIGAgECCQIKAQIBBgMBAQMGBQUBAwEDDAUCBwEBAwEIBAsDAQcDAQUDCAUDAQYBAQECAgMEAQMHCQMDAgUIBeMECAICAwEBAQEBAw0IBwoBAQEDCQYCAwEBBgYEAgMDAwYBAQICBgQGAgMDAQMBAwIDBAECAQIBAQMFAwYEAQ0KBggEBgECBQIHCQEKBAUECgEBAQIBAQEECxEUBgEKAgEBAgMDBgUDAQMDAgEHEAQCAgEBAQICAQIDAQMBAQEBBAQBBAUEAQICAwQMBAIGAwgDDRAMBwQFBgoDBg8FCAgBAgEEAgMBkgIDAwQGBAQEAQIFAgEKCAcFAQEFAwYFAQMDBgEBAQMDAwsFAwEBAgEDAwQDCAYDBAEBAQEDAwYDCwQGAgMEAQUDAgIDAwcFAQEBAQQEAwEDAgEBAgECAQIDAgIFBAMGAQQFBwYEAQIBAgMCAgEBAQEBAQECBQMCAgEDAgcDAQMHAgIBBwQGBAEBAgIBAwICBgMDAwEFAgkMBgMGAQQDAgcBBAMEBgYFAQEBAwYDBAUBAgIEAgEDAgICBAUCDgMCAQYCAQQCBwQFAQUCAQECBwMBBAcDBw0EBgEFAwIDAQEBBAEBAwgBAgIDAQIBBQMEAQwECggBBgMBAQEBAQUEAgMBCA0DAQEBAwQCAQICBQYCAgEBAgEFEAIGBA0KBQEDAwEDBgUGAQIJDgcBBAIClwICAQgDBAEKBQEBAgQNAwYCAQQBAQICAQYGBgECAQQJCQkMBAECAQEBAQIBAQECAQkNAQQHAQwGAQEBAgQCAgECBggEAgECDQcCAQECCQEGBwQKBAkFAQEBAgIDCgEFAQEBAwEDBQEEBQIEAQEBAwYFAwYEAQQEAgUOAQIHAgEBAwwBAwIHAQIBAwICAwQJBgECBAEBAgQBAQIDCwcCAgICAQcJBggGAgEDAQECBQUBAQEBAgICAQMDAgEBAQEBAQQCCQsDCAcBBAEBAgIHAwEBAgEEAQECAgIBAgEBAQEBAgMDAwMDBAMDAQEDAQEBAQECAQIBAwkCAgIBAQEBAQoEAQEDAQIBAQEBBgQGAQYDCAQBAwMCAQEBAQ0BBwICAQIDCAMBAQUDAQEBAQIBAQMFAgEEBAUBAwkDAQEDBgQHCgUEBAMCAgUGBQEBAQEBAQMEBgIEAQMBAwUJCgUBBAEFBgEDBAQEAwECBQYBAQIBAQEBAgEBAwIBBQMBAQQBAQMNAwIDCggDAQIBAQECAgMBAgMIBQUBAQUDAwEEAQEBBAECBQYCBAEHAgEBAQUBAQQEBQEDBAgGBAIBBAICBgEDAgICAQEBAQMBDAECAQMDAgEBAQQBAgEBAQECAQEBAQQBAQEFAQEEAgEBBAEDAgMEBAIBAQEGAQcBCAEFAQgEAgICBQMBAQEBAQIBAgEBAhAHAgQCCwkEBgMECgYLKAQDAQUCAwIDAgEDAg0BAQcBBQ4MBQECCQUBAQECCAICBgMBCQQFAQQCAQIGAgEIAQEDAQUBAggTAQEV/AEBBgUHAQQDAQMBAQICAQEICgQCAQICAQIDAQcGAwEFBAEUAQIBAQEEAwEBAQIHCQQCAwQBBQYBAwMBAQIBAwQDBQQBAwEFAgMBAwkGAwEHBAEBAQEMARoBAQkCCAEBAgYNAwINAQYLCwMBBAQBBQEFBAQHCAMGAQEBAgIFAQMEAQIHAgMBBQICAQMBAgECAwICBAIFBAYDAwkFAwEBBAUBBQEDAQQKBgcGAgICAQEDAgEBAQIEBAIGAQEBAwECBQYBAQEDAQEIAQMBBAMBAQEBAQECAQEBBwIDAQIBAQYZAgYBBgkCBgYDAwwICAEDAQMCAgkFBQMBAgECAQMJBAQCAwEECwIBAQIBAQECAQYEBQIBBAEDAgUCBQIBAQQCAgcEBQEBAQMCBAIEAQEDAQQFCQgGBQIFAwMBBgEBAgECAQMBAgQEBQMBAQMDAQMBAQEEAQICBAYBBAMFAwMEAwIFAQMCAgMDBgQDCRoBBwUBAQIIAQMJAQcCAgIBAQYBFAEBAQMBQwEBAQMCCAQHAQEDAQEJFA0HAgMCAQIEAgQBAQQDAQEDCAIBAQEGBgMBAQEBAwECAwEBBQICAwMCAQICAgMBBAIBAwoGAgUBCQICCgkBAQEBAwIBRAkFAQICAQEBAQEDAQkEAgEEAgECAQIIBQMFBgoBAQIBBQoBAQECBwIFBAMBCAUHBQEBBAMEBAEHDgoGAQEBAwMCAQEBAgICAgECBAoDBA0SLQEBAgEBAQMBAQEGAwEFAgQDAQEBAQEBBgMCBAIEAQECBAUDBAEDCgIDAQEBAgIUAwMFAQYFAQICAQEBCQMDBgIFAQEHAgQCAQEBAQEBAQcCAwMDAQEBBgYBAQEPGwEBBAIBAwMDAQEBAQEBFgIBAwMBAQoBJQcIBgEBAgEJAgEBAQcGBAIBAQMEAQECAQEBAgEFBAIFAwMEAgMCAQMHARYEAQcGBAUDCAoCAQEEAgoEAwECAwgDAgEDAQIFAQEBBAEFAgMDAQEEAQEBAQUEAwEYEwQCNAEGCwgEAQMUCgMFBAIGAQMCAQICAgIBAwIBAQUBAQQPCAUCAQEHBAcEAQQCAQEFDQYBAQMCBQwM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AQEIAQMCAQMCAQYFCQwHEAIGCgYGBAICAwEEBAMCBAQBAwIGAgQFAQcHAgMHBAkFBwUDAgIHAQkCAQMMBg0KBgEBAgMCAQEDAwUCBQgDBQEFAgEFAQMGAQMDBAsFAwIHFQgCBgYHBAkGBgUDCAIECQIECA4MCgUMCwMIAQEJAwJXBwUGAgQBBAMEAQIPAwQNAQ4RAQEBBgsDBAUHBggCAQQCAQECAQEBBwQBBwMBAgECAQEBAQIBAQEDAgUEAQEBAgQEBwsEAgIBBgYDAgIDBQIEAgUEBAIGBAEDAQIDBQQDBAIBAwMBAQEFCAEBAwEBAQEFAQECAQEDAQIBAgQCCwcBDQgKBgEBAQECCQICBAECAQEBAQEBAQEGBAUBAgEEAgoSCwoEAQYBBAEBAU0BAwMCAQEBAQEBAQEBCAEBAQEBAUkIAQYCBAQCAgMGAgwbEwQFAgQEAgEIAQUIAggCCQUKBgICAQECAQUBDQIIBQQFAwEDAQEBBQQCBQQIAwQCBwcJAgIDAQIEAgcFAgIJAgYBAQEKCAI0BAcCBAEBAwEBAQMBAQEBAwMCAwEBAwkGBAICAQMBAQEBAQsFAgQEBQMCAgEEAQEBAwMBDAIBAwQBAwQCBQIBAggQCBgKBAUFAgEDAgUBAQEEAQIBAQEBAgYFBQsFAQQEAgICAgIFAiABAhwyAQIEAg4HAwEBAQECAQMDAQMBAR8gG1UDCQEFAQYBAQILBwcCAQUBAQEBBQoDAwECBggDAwEDAgIDAgEDBQMCCQMCGAUEAQQCAwEBAQIDBQEFBQQEAQIDAgwBAQIGBgMBAQUMBAEGBAMEAwEBAQEBBAEBAwEGBwMZAQEBAgMBAQMCAQUaYgECBQQCAQYBAgUIBQIBAgIDAwMCCwEBAQEEAQIFBwcGAQgDAiIFBwICAQQDBgEEBAUCAwEBAQIEBAQCAgQCAQICAQECBwECAQMBAQEBAwQDAwIEAwIDAgEDBAICDwIICwIECggCAQIBARIMAgYDCAIDFgIBBQIFBgUFBRkGAgIDBAEHAQwFCQUEAQEFAQECBwoDDwEJBAYFFwEBBQMBBAUCBgIBAQEBBgYGAQQEAgEEBgUICwcIAQEEAgIEBQIBAQcDCAEEGh4DAQEBASEdDQEBAgEBBQECBQQCAgIBAQQKAwIHAwEGFg0JCAEBAQIDAwIFBggXBgICBAoFDwwCBAIDAgEGBgQHAwEBAQQHBgYHBgMCEQwGAQMEAgEDBAUCAQoFAQIEAQIIAgMBAQULBQMFAQECAwkLBAEDAQIBAQIFBwIKCQEBAgEEBAIGCAQDAwMDAgMBAwEFCQQFBQIDAgEHAgUHBwcGBAECAgIBBQcFAgQCAgEBBQ8GCwUEAQIBAQEBBAICAgEOBQYBAQEBAgECDgMF/tkCDA0CAQMCAgIHCQgCAwECBQMGAwQGBAMBAgICAgUBCAIBAQIFAQEECwIEAQQEAwQFAgECAgIBAQMDAQEBAQIBAgEBDBYGAwMBAwICAwYCCAQBCQUCAwYBBQkDCQUCAQIBAgIGAQ0GFgUCBAULAQoFAgEBAQMBAwMDAQQBAgUCAwQHCAYFAgEBAgECAwICAQIBAwIDAwEBAgEDBQIFAQMBAgICBAsIAQYEAwkFCg4CBwIEBAQECg8CBwECBgcGAgkIAwMDBwMBBAEBAQEBAgMCAgMDAgYOBQIBAQEBBAECAgYDAg4ECQMHAgIBAwwECAIBBQQIBQUEAQEBAQMBBhUPBAgCAgMDAwECAQEBAgEBAgoLAQQCBAEDAQEFAQICAwIGAgIBAgQBAgIDAQECAQUEAQEVAQIFCw0JBgMDAQEDAgEBAgYBAgICAQQDAwUFAwYBAwUECg4BBAEBBAcIAQkBAQQCBgMDAQEEAQEEDAoEAgICAgQLAQQBAQMBAQMEBgIFBAECBggDBgICCAMBAwcCBgIGBAgDBAQEAgIBBAkFCggCBAEBAgIBBwUQBAEIFQ4JBAICBAICAQMIAwMCAQEDBAQBAgMCAgEBAQMIChMBDgQHAwkOBAELAQECAgQCAQMBBAgEAwYCAgUCCQYRCwYBCgIDBAECAgICAQEEAQQHAgMBAQEFBwICtgsKAQEBAQIBCAcMDAcCAgMFCwYBEgwCBQIBCQQHBgICAQEBBwUICQgCAQESCQgCAQEFAwICBgEBBQwEAQEBAgQBAQIBBAIBBAEBAQMOBgECAgECAwQFCQgDAgIEAQEHAwgDDgkBAgECBQEEAwEIDAIBAQEBAQIGBggDAgEFBgEEAgQECgQEAwUIAwIBAgIBBAgDAQIBAQICAgIBCAQIBgoHAgEDAwEBAwIDAQkKBgEBAQIBDgYGAgEBAQEBBgEBAQYIBAEKCQIEAgEBAQIGGggBAQUBAQMCBwMCAwEBAQEBBQQCAQQCBQIDAQYBAQECBAELAQIBAgUFAQEBAQECAwMDAQMCBAoCBQUBAQEDAwIDAQMMBgEDCAMFAQUEAQQBAgEBAgECBAQBAQEBAgQGBwUBAQEGBQQCAQEBAQIDBg4EAQcHCgsBCgMIAgECAQIEAQEDAQIEAgICAwEBAQIHAQEIEgIHBgQCAgECAQQHAgUBAgQCAwYBAwEBAQIDAQEEAwICAQEBAQMEAQMDCAUPCAIBAwIIAwICAQEBAwECAQEBAQQCAgQEAwMBAQUBBAECAQECAQEGAwECAQoEBQMHBgIBAQIBAwQFEwoJAQMFBAMCAQMBAgIBAQEBAgQBAwEEAgEBAQEEBQQCAgICAgQCBAUGBggBAgQBAgUFAw4MAxQYAQEBAQIBAQEBAQYBYwGdAgIBAwMEBQMCAwECAQQCAQICAQEFAQcOAgMBAgMCAgHiARwEpQO0Ab4B0wEDAQICAQEBAgQFBAECAQEGAf7aAQEL/gIBCQEB/uIBEv78AQEJAQEXAQECAQICAwEOAQMBAfMBDvMBAfgCAgoEAgIBAgEBAgICAQUBAR8BAQYFAQMBBgYEDQEECQUEAQEBAgQCAwUBAQECAQEBAgIBAQEEAwgBCgUSAwPUBAkFAQIHCdHBAq8IBAECAwMBBwUFAgIBEwcDAwIaBAIBAQECpAEBA5spAgICBQEGAgUDBQYEBAQEAQMCAgMECAMEAgMCBggDCQYBAwUEBgEBAQEBAwEBBAYBBQNPAQIDBBkEBQM6As8BAQEGCAICAgQBAQEBCAIBAwMDAQICAgoDCwIBAgcKBQYEAQEBAQIEBQIXBQUBAgIBAQUGAwMDBgIDAQQBAgYBAwMCAwUEAgUCBgECAwQEAgEBAgcBCgQBCAIECgMBJQoCAQgBAQcuAgIFBAgHCQIGCQIFBgEBBAgTBwEBAgEGDQUFBwQCAQMEAQEBAQMCBgIIBQEGAwMBAQIDBQEBAQYCCg4KAgEDBQQEAgECAQEDAwMPAwcCAQgGAgMBAQECAQEDBgEDAQEBAQUBBAUCBQEKBQQJBQIEDAwJBQIDAwYBAgICAgEBAQQIBQEJCwkHBgcEAQwIAQQHAgECBgIGBgIOBQkNDwYHBQIEBQUGAwkDAQQCAQEEAwcDAggCAgEBAQECAgMBAgMGBAIBAQEBAQYBBwoDAQEBAwIDAgUBAQECBAECBgIBAgQDAg4BAwEDBAQEAwEBAgICAgMEBQcBAgICAQECAQICBgMBAQECAwIECgYCDgYIAgMCAQICAwYGAgEBAgQCAQIBAQULAQMDCwsGDAECCAMJBAEBAgFMAgEBTgUCDzYBNg8BAQICMjQsARQMOgEBVgMCBQIBAwQCAgECBQUSHA0EBwMDAgECBQEDBQIBBQwEAQECAwwCBgYEAwMDAQICAQkIAwMBCQIJAgIDBAQEAgEBAgEJAQYFDAICBAECAQECAQYKAQMJBAEIAgIJOQEEBQECBAIEAwIBBAMDBggGBQMCAQECAQEBCQYMAgMCBgUBAQUCCAQDBAIEBgQDAQIBAQEEBQ4aCwEIBgQFAQEFAQECBgQCAQcFAwEDCAEEDAQCAgECAwQCAwQOASQDAhcDAgEBAgIGAQEHAkcBAgkDAQQEAQMBAQEBAgMDAgEFBwQBAQEBAQECAwMBAQoDBAIDAgEDAQECBQIOCAEQBgMfBQgBAQIHAQIIAQEBBAQBBQUDCAECBQcDAQEBAwICAQMCAgECAwIDAQsGAQIDAQEBCAMIAQENAQkBAgEFCgEBAQQDAQMBAh4CASACAQEYAQIKAXwDCgECAwIBAQIBAQECBgEDAQIGBQEBAQQDAQEBAQECBQIEAgcBAwMDAQEBAgEBBgIJCAQEAQEBAQIKAgIDBAICAQIJAwUCAwQBAQMFAQEBAQEFBAICAQEBAgYEAgYEAgICAQMfAgMBAUgCCwMIBQUCAQISDQEFBQEGAwQBAQECAgEEAQIRBAEBBwIFBg4JCQEBAwEJBgIFCQIDBwMCBwoBAhQBCQwBBAICCgcBAgsBAggGFwIFAQkGAgEBBAIDAQEKBAIIBgEFBQQCAgQDDQYIAgEBAQMBAQICBgICBQQBBgcCAgQiEh4EAwEBAgIEAgQKAQIHDwMGAR4PBQISCQcQESUOAQYFBAEEFQ8FAQEPBwUSBxQRAwMDBBIgBQYBAgMBBg8CAisB2gQBAQEFIBkBAyEvDAYCBwoGAgYhMQEGBCAZCQICAgMFAwICBwIGARAIAwQBAgEBAQMfGCklCAcEAQIDDw4BAQgCAQQCIzACCQYkFgoCAgICCgEBAhsECwECAQEDBAcEAwIDBAMMBwMFEAgYAgQFAwIDBwEEAwIBCwUJIwYEDRUCAgECBAEIAgEBAwICCAEFAQYBAgQIBgMGAgcGAwEBAycFAQgBCA4PFgcDCggEGQMHAgcFAQEIAQYGAQcFAxcNAQUBBh8KDAQSCgEBAgQEAgYFARkBAQUFAQ0IAQMBCAEHAQIEAcgTGAgLCRAIBQcBCQcEAQEBAgkxAwYEAwYFGBUBBQMDAggQHgYMBB4PAQECBgEIBgoPBQ4IFhIBAm8UFAYHAgYBCxUXAQEIAgEGAgEUGgoMCQkREQoGBAIBAQQSAQUHAwMIBgoMAwwGARISEBwKCEoBKQFXAQE1HwsICAYNBQgGEx0QIjEZAgESAgEgGQsSAhYPBgwHDwIGAwcFBAYREQMBDAICAhcKBAYCAQEBDggLBQ0TCRIYHgICATIECwMDBgEHFAYBBQEVAgEBHxwTDQEMBA8NCQ8BBgUZCAUBAgcJEBMcAQIXCgk+WAEBwAUHAgQEIgECEzIuBQEBAgIEOQIGAgcMBwUCAQ0BFwYCBgECBAsHBAYDGwgBAwEBIyg7CQsRCgoCASsBFRAWAgMFAQYIBAoHCQMGBQYCBAkTERQBDxMaGCAKAQcHDyIUCxQGDQsHAgQBAwIEBwIBBAULFR0XCwgKEAUOCBMVBAgIBAsJBQcMEzASFQ4KBQEDIxcgEBVHEQUHAwcGBwECBgUBFQ0SARocHRoHCQEBBgcNEA8kDwsGBAgGAQMDBQsDAwIGAgILEx0JAQsIBRMGChMUCRYTBAcHBAYBCAUIDRU0KREHCB8bHhQXAgEXDhgDBQEHCQYKOv7tAVRiBwcBAwgWDAoIAgQEBwYGCRwcCgQBCAUOBgUBAgUKChAIAQEDBAURBwUIDgsQEQ8MBAMJAxQvAf32HzYgIDYfBAEfNiAgNh/7/wQBGSMjGfv/GSMjAkIF/k4FOBMTFRZ1T01NUDwQOpNISm4UFCEzAhMBLSw7UE1OTzwFAbUFKywPASgn/mE5lEhKOCksXAMGBwIJAgEDAQEDBgECBwQEAgEDAQQFAgECAgUCBwUEAQoDAQMCAgIBAgIDAQEGBQMCAgMEBAUBAgECBgQCAgIBAgIFAgEDAgEBAQEEAgEDAgEBAwYBAQEBAQEBAwILAQIBAQEvAQEBAgEBBgUEAgsDAQUBAgYDCAQEAQECCQgGBAEBAQECAgMEAwkLBAUICAUEBQsDAQIDBAEOAwICCAoFBAEGAQQBAgUDAggCBAIBBQwGAwcCAQgDBAsFAgUFAwYHAQYEBgUODgcBBg4ECwUKAQUGBAcFAQQECwMDAQEBAgEhAQUMCAMFAwQDBgQDDQ8NAgcIAgIBBwMLCQgHAQEIBAQEAwUEAgMDBgIBBQQCAgQBBgEBAgMCBAICAQEBCAkCBgIBAwUCBwkBAQEHBQQCAgICAgQGDgQDAwMHBwICAQMFAgIDAgEBAQICAQQEAgECAQEBAgEBAgMEBwIDAgUFBgEBAQEBBAEEBAEDBAEBAQEDAQEEBAEBAQEHAQMDAQIFAgUGFgQBBgICAQEBAQEDAQEBAQJCAwQCAQQGBQQDAQEJEhoMAgUICgYIAwIEAwQHDwQBAQIFBQYBBAMGEQQBAQIBAgUHAQIBBQEBAQgIAQQCBwMBAgIBAQEECgkBAgICAQoCARMJCAIBAQMFAQQBAQEEAggFAwUFAwEDBAEBAQEBAgcIDwEBAgEBAQQFAQIDAQIBAwkJAgYCAwQEAgIFARkQCAgCAgECAgUDBAQBAQICAgIBAQEDAQcCBAQCAgIEAgEBAQMBAgYBBQICAgYIBQEDAQEBAgIDAQEBAhEBAQc5AwEBAQIDBAYKAQcFAg0DBgIDBAIEBwEBAgEDAwcHAwICAwQCAwICAQEDAhMDAgIEAwUDAQEBAgIEAwECAgEBBAQDAgkBAQICCAQEAgMEBAYCAgEGCAkBAwEBBAUCAQEBAQICAgEDCQEBAQEBAwEBAgMGBAIdNgECBwEEAQQFAQQDCwUJBQMBAQEBBAILCQIFBQIBBQIGBAsEBgcEAQQBAR4CBAkFBAcHBAYBBAICAQQFAwYCAgECBAMEBAIBBQgEAQEBAQECAQEBAwkCAQICAQIBAQYEBQQEBQQMAgQFBQgNAgECBAEKFwUBAQIGBgUBAw8BCAIFBgQQGAMCAQQBCQ8IAwYBAQMBAwkBAQEBBQIKCg0JBQMBFBIBAQUIBQUBBgECBQMGAQMCAQECAQIGBQIEAgQKCAkCAwMBAQICAgQEBAMDAQYCAgsJAgIBCjwTAcwEBAIEAQUCAQECAwICBAQBAgIBBgEDDAsDAQgBAgEEAgQBBAETAQECBA8DBQQDAQEBAwEFAwICAgMDBQoGAg0HChQDAwQCAgEDAwEBCBEEAQkMBwECBAoCBQYGCgECDQgEAQQCDwgGCw8JAQEBAQECAQELAQEBAgMCAQMCAwQEAwUECAMBAQECCQQFCgQFBQECAw8HAgUEAgICAgEFBggCCAIEAgIEAwECBgUJCAYEAQECAQICAgUEAQEKCAMFAwIBCAkF/t0ECwICBAICAQIEDgMJEwUBAQEBDgYBAwMEBwUDBQIEBQUCAQkBAgkFAQcCAQEEBAUCBAEBBQMDAwECAQQBAQUDAwYDARIMCAoEBgMCAgMFAQoLAgwFBgEFBQgBAQEBAQIBAQUOFgwHCgUDDAICAQMDAQIBBwcDAgIDAgMBAQkVBQQBAQEBBQMDAgECAQEBAQEDBAIBAwIFBQEDAwICBg8FBRcBEQoHDAYKBAEIDAgDBxMHDAgBAQIEBQEBAQO+AwUBBgQLAgEBBAICBgMBAgMJDAgGAQICAwEBAQEBBwIEAQEEBgcEBgQNBwMCAwMDAwEEAQQKAwYEBgEBAQEEBAgCCxMCAgIGAQEGBQICAQEBAgEBAggGAQIBAQEDAQUCAgEDAQUBAgEBAQIBAQEBAQUDAQcHAwgEAQMGCAQCAwEBAQQDBAMBBAEDAwYCBAIDBwIDBQIDCAEBAgEBAQEBCAsHAwEBBAQCBQMBAQEDAQMEAgYEBgcFCgUEBwIFBAEBCgEEAQoOCAEBAQQHDAQCAwIFAwEEAwICAwUHEgMCAwIFAwUECgQHAgUEAQEDCQEEEQwOBAIIAQcDAQQDAQIBBAEBAgIJBAIBAwMDAgYEBQEMCQ4IAQYCBQMDBgcEAwELCAUHAwQFAgMBBAYIAgEEAwIGFAYKDg8CBAEBAQMEAgEIBgMHAQQGAwMJBAgGAQYBA7sBCgEBAQEBAQEEAQwFAgEBAgYPAwYEAgIFAgIBAQEDBQEFCAIDAwQLCwsQAgEBBAEGAgQBBQoSAQEDAQEDBgELCwEBAQMBBAUCAggHCwEBAg8JBQECDQgHAQ0IAQoGAQEBAQECAQIMAQECAQIDAwQBBAsGAgQCAgMBAgMIAwIBBgoBCAMDBhMBAQcDAwECAgEEBwUEAQEDBgEEBAECAwEBAQMGCQkBAwIEAQICBAECBAMNAQkCAwIEAQcMBwgCBQMBAgQBAQYFAQEBAQQFAQcCAgEBAQEBAQICDg4IDQEDAgUBBQUCAQEBCAQIAgICAQEBAQcBAQEBBgQBCQgBAQECAQMBAQEDAQEBAggMBAEDBwMIBQIDAgEBAgEBAQwFBQEBAgEEBgMIBQECAQECAwYBBAEDAgQGBgIDAQEDBAEGCQIDAgEDBgcMDAQCBgUBAwEBBQEGBAECAQIBAgIEBAYBBQEBAwEBAgUMAg0HAQIEAgMFBQIHAwUIBAgFBgcBAwEBAQEEAQIEAgQDAQEEAQECBRICAQYCDwsDAQEBAwIDAQIDAQEEAgECAQIDBQQGBQEICQIFAQUCBAEDCAIBBAkFAgEBAgMBAwMBAQMGAQQHDA0BAwIEAgEBAQUCAgQDAQEBAQIBAgQBAQEEAggCAQICAgIBAgECAQIBAwIGBQIBBQMDAQIDAgcIBAEBAgEBAgEBBAIJAw4CBAcGAwcFAQEBAgQBAgIBAQEBAgEFCQEBAQYEAgEBAgEEAQEMAgEBAQECAQMBAQEBAQIFAQgEDQYBAQMJAwEDBwEDAQECAQEBAQEHAgMCBQgFBgkBAQkHAwIBBwEBCAUKCQQCAQIGAgIBAQEDBgEhAQEIAgECEAwLDgIEBAIFBgIDAQEBAQEBAgICBAEDAQMBBAwRECQGAwEBAQUBBwMGBQoFAwIHAgIDAQIHEAUJAQQEAQMGAQICAQEBAQEHAQEIBQMCAggUBwMDAgECCAgCAQMCAgcCAQMOCQcFAQoIAgMBBAMBBQMBAQIIBQEHBwYBCRUBAgECGwEDAi0BRQIIBggFBQICAQEEAwEBAQcFAwgFAgEBAgQBBAgEAwECAggGARYDAQICAwIDAwMBCgkDBQUGAQEGBgEEAgEEAQIEAgEEBgIBAQICBAEDAwECAwgIAggDAQYBAQwBAgEXBAYGCQ4HBQ4HDg4FAgQEAQUBAgMBBgQKCgEKAQEBAwMGBAYBAQMDBQEGAQcBAwIEAQECBgMHAgMHAgcDAwwDBAQBAgUFCQEEAQQOCAoHBAQBAQIBAQIBAQEBAQEDBwMJAQEGAQUKAgEEAQgDAQIDAQYGAQEBAQEBAwIBAgkCBAECCB4FAwEICgQGBggFGQ0EBgEGAwMLCAgFAgEDCAUEAwECAwQOAwECAwECAQEBAwIBBQcFAQMCBAECAwICAgIGBwQCAQICAgICBwUDAQEBCwUEAQEFAQIBCQYBCQgFBwEBBAEBBQYBAQIBAwEBAQEFAQECAQIEAwMDAgECAwIBBAQBBAIBAgIDAwQEAwEBBAMFAQIFAgQCAQIHAQQBAQUGAQUBBAICCCIBCQoEDAEKAgIBBQEBAQcFBQ0CAQUBAVoCAgEBAgsIBgEDBAEMGRAJAQECAgYEAQQCAQYDAQEBCAUCAQIGCQQCAQIBAQMBAgMCAQEHBgQBAwMBAwMBAQYFAgEBAQMPBgEBAwYBAgIHAgECAQQNBwIDAwUCUQIGBQIBBAIBAgEBBQECAQ0FAgQFBwICAgEGAQEHDgEBAQIIAQUEAgECAQwMAgYBAgQEAwECAgoUDgkCAwMBBAMBAQECAwICAwQLAwwJAwEFAgMEAgEBAQEBAgIKARADBAUBAQEJBzwBAgECAwEBAQEBAwQDAQEGBgUCAQEBAwIBAQcCAgEEBAIFAgEHBgMFAgQNAwQCAwIVAgMGBwcBAQMEAQECAgMBBwIBBAMEBQMGAwQEAQEBAQICBAcHAgMBBQoCAQEBAQEBCRACAgQDAwEBAwEBAhoCAQEFAgEBCgE1DAsCAgQBAQECAgIMBAECCgkEBQEBAgQFAgEBAQICAgQHBgMHBQIGAwQEAgEEDBoEAggGBQMCAQkOAwEFAwEBAQIBDAgDAQQIAQQEBwUFAQECAQEDAwQCAQIBAQEBBAIFAwQDBQMHBAEjARoDAUsBCBAIBQEBBAsUBwEEBgcIAQEBBAICBAMBBAUDAQgCBgoNBwIDAQECAQIJBQgFAQEEAwIBBgEBDgEHAQEDAgcHBgEMCwQCARYPAgUIBQ0BAQMFAwMBAQQCBgYBAwcBAQEGBwoHBAQBAwEBAQEBBAMEAgIDBAIDDP7lCAEDAgEFAwQIAwoWAgIGIQgIAgEIAQIGBgoBVA0DBA0NBgEDBgcBAgUEEQULAQEBCAEHAQEJBgQKASIBBE7DHg8CAQcIIxcBAQsEAQEHBAIBAQkFBB0fCQ0QFBYUDQYEBAcHHDcSAyIRHg4DAwIDAQUlFAMJAgIBAQ8IAQIBAgkFBiIaDQ8BERQMLAYDAQEBA00WEwECAgMFBg8GBQUBBQEBBgMBAwYrEwECAwMHDgMFBQQBOQMEIh4aRiECBQEDAwQDAQ4BBAEBAQcGAQcEAQIBBAECAwIbAgEEDAUCCA5dCyYUAgMHBgcHAgMCAgsBBAMHAgIYERYMAgcBBwETFgMBAQoQDBQMAwUBEQQBAQIBAQEDCwECAgEGDAEDBBcUFQ8CAwYNAQH+tBsKBAcHBQkBDhUOBwMEAwkCBAIEBAMSCQEDCAYBCwgUBAoBAQEKFAMVCRAHAgMJGQMCAjwFAgQIDgwBBQcKBgMBHAsFAwcWDxYKDAQDAwERAxcKCxYJDwYBAwgXCBIIBjABI3oLHzUBAQkBBAkGKh4PIgICDgsHEQEGCgYOARgeDAsHCwEEBwoHHBMDBjALARYREQICAh4RBQwCAQoICwsYEwoJDQMBBYQFBgYKDRoIBwYjHwEBBQwJDQ4gHgwJCgUbEhAMBgoHCggJBQUiAh4RBRQiAQFkCgIBFAIMCQcQAgIGBAUOAQUBBAcDAgMbAQMTGAEEAgEMAQQIAwsDAQ8FAQIBAQEVCAoZAQQGBAVpGBwTDgEECw8HHRkNDAcHDhgZGAYDCgYMExkrExMLCgUDCSEVIhoHBAkLCgIGAQUDBAEDAwYHCQcKBQEDBgECAwERCgUHCQ0PESMZHwECBwkOExXwEQUHCxcWGwQCCA0DAQ8QFCscDwoEAgkHAyIPIA0LCggFBAkKBgMHAQEBAgEDBgQDCAQRBQoHBAYBAgIECgsFCAkOISYSJgULCxcXIBscERALDwceGxEL/spAAgHfBgcIDBMOBAECAgYFAgQRGQ4JEwQLBgUGCw4GBgMBAgMCDwYEAwYKCh4QDAkYLgMBAiAgNh/9CR82ICA2HwL3HzYgOSMZ/QkZIyMZAvcaIgAAAAcAAP9rA+cDSAAdADkAXABsAHwAjAD8AAABIg4BBw4BFREUFx4BFxYyNz4BNzY1ETQnJicuAgcyFx4BFxYVFAYHBgcGICcmJy4BNTQ3Njc2NzYFFhcWIDc2NxUGFTAxBgcGBwYHBgcGJyYnJicmJyYnMDE0JwUVBgc0FQYHBgcGBzU2NzYlFhcWFxUmJyYnJic0FTQnBTMyFh0BFAYrASImPQE0NgURFAcOAQcGIicmJyYnJjURFhcWFzMWMxUjIgYdARQXIyIGHQEUFjsBMjY9ATQmKwE2PQEzFRQXIyIGHQEUFjsBMjY9ATQmKwE2PQEzFRQXIyIGHQEUFjsBMjY9ATQmKwE2PQE0JisBNTI3Mzc2NzYB+GKuhygWGS8niVZZwVlWiCcwDQ0WKIavYl1VUnseGwoOIDt1/mF1OiEOChohOz1SVf6mJEN9AaZ9QyUFBAscKzBDRU9WVk9FQzArHAsEBANvBAEECxYwWoePYDH8rBkya6KbYzEWCwQEAYd+BQYGBX4FBgYB7RsefFFVuVZRPjshGiArbKcDLRaqCQ0EFAQEBARKBAUFBBIElQcaAwUFA0oEBQUEEgeXAhkEBQUEUQQFBQQQAg0JqhQoFAWcXioDSBgtIBAqGP2SLiUfLQwNDQwtHyYtAm4YFhMRIC0YNwwLKRkUEwgOCRQMGhoLFQkOCBQTGhMVCwzRFQ8bGw8VNwcIBwgRDA8JCgMFBQMKCQ8MEQgHCAZRKAYIAQEGBw4OGAk9CRwPDw8PIAc9BxwODgcGAQEIBg0FBFAEBQUEUAQFQv7jExQZKQwMDAwVExoUEwEdEQwdCAI0CgdXBgQFBGMDBQUDYwQFBAZFQwcFBQRjAwUFA2MEBQUHQEUEAwYEYQQFBQRhBAYDBFcHCjcCAQkbCwAAAAUAAP9rAugDSgARABwAHwB/AI8AABMiBhURFBYzITI2NRE0LwEmIwUhFRQXHgE7AREhARcjBSIGHQEUFjsBBxUjIgYdARQXIyIdARQ7ATI2PQE0JisBNj0BMxUUFSMiBh0BFBY7ATI2PQE0JisBNj0BMxUUFyMiBh0BFBY7ATI9ATQrATQ9ATQmKwE1JzMyNj0BNCYjBzMyFh0BFAYrASImPQE0NiMKEA8KAqwKDwfeBw7+TgGYAQIOCcj9hgHLkpL+/wgMDAhiAZcIDAESCAhDAwQEAwsBhRIDBAQDRAMEBAMLAYMBEQMEBANDCAgLDAiXAV4IDAwIwpwGCAgGnAUICANJDwv8VQoPDwoCyQoI3gwy0AMDCQr9bwNVkp8JBn4GCARLDAhgAwIHUwcEA1MDBAIDTEwCAwQDUwMEBANTAwQCA0xMAwIEA1MDBAdTBwMCYAgMSwQIBn4GCRsFBE0EBQUETQQFAAgAAP9qA+YDUgBSAGQAawB0AIgAkACYAKgAAAEVIxUmJyYjIgcxBgcGFxYXFhcGBwYHBgcGFBcWFxYXFhcGBwYHBhYXFjY3NjcVMxUzNTMVMzUzNTM1IzUzNSM1MzUjNTM1IzUzNSM1IzUjFSM1BTIWFxYXFQcVIgcmJyYnJj4BBSERIRE3JwciFQcGByYnNgcWFwcGByYnJicmJyY0NzY3Njc2JRUjJyYnNjcHFh8BJic3NgcWHwEVBgcOAScuATc2NzYCAyhVRE8rFwsLAgEGDCYgIEo2RSIRDQsLDREiRTZdJiEnDggCDBBBJk5pKA/KDybR0dXV0tLPz9TUJg/K/rUTPCRFXAFTZSceJQwFAg4BMwEY/ugGBh0BASA1JiRQYisnESAOWEI9Jg4MCAgMDiY9NAERBRMhHhs7YBQrCEM8Dxs0QVcLa1EmNgkGAQYMKBsDUs45SigxCwoTEBgtQDMsCA0QEwgNDRsNDQgTEA0KNDZAMBcjCw4QFy5eT8jIzs4pD3UPXg5yD3UOIMjIznMYFipPCQlJCTQxPyoXGA5q/dIB5wsGcQEBIj8sLwgJOSwWKBMIDw0UBwwKDQoMBxQNDAi8EyMiIkFuFy0JAgQTI0QFAgtUYDAXDQkGFxgqQi0AAAAABAAA/2oNuwNSAAkAEwAdACMAAAEDIQUDJQUDJSEBAyEFAyUFAyUhAQMhBQMlBQMlIQETIQcTJwvIdv6DATV2ATQBNHUBNP6D9bV2/oMBNHUBNAE0dgE1/oMEfXX+hQEzdQEyATJ0ATL+hQRuXwE5/WD7A1L+g+v+guzsAX7rAXv+g+z+g+zsAX3sAX3+her+herqAXvqARr+z7/+yr4AABL/9/9qCKgDUgAOADMAWQCIAJcApgDOANEA+AD7ARgBMgFKAWYBfQGYAaEBqgAAAQ4BFRQeATI+ATQuASMiJzMVFzcXBxYXMxUjBgcXBycGBxUjNScHJzcmJyM1MzcnNxc2NycdAQYHJwcXByMVMxYXBxc3FxUzNTY3FzcnNjczNSMmJzcnByc1BzMVFzcXBxcWHwEzFSMVBg8BFwcnBwYHIxUjNScHJzc1Ji8BIzUzNyc3Fzc2NzMXIgcOARUUHgEyPgE0LgEHMh4BFA4BIi4BNTQ2NzYlJxcnMycXJzMnMyczJzMHMwczBzMHNwc3Byc/BR8GBQc3Ax8BIxcnFyMXIxcjNyM3IzcjNwc3BzcHNxcPBi8FJTcHASIHBgcOAQcOAQcGFhcWNjc+ATc2NzY3NicmJyYHMhceAQ4DBw4BLgE3Njc2Nz4BNzY3NjclIgcOAR4DFx4BNz4BJy4BJy4CJyYHMjMWFxYXHgEXHgIGBwYmJy4CJyYnJjY3NiUiBw4CFBcWFxYXFjI3PgE3NjQuAgcyFx4BFxYUBwYHBgcGIicmJyYnJjQ3PgE3NhMiBhQWMjY0JgcyFhQGIiY0NgdhGyIbLjYvGxsuHBUkcyZEUkUKBV5dBg5CUkcKEnMiRVFCCgdbXBBCUUUKGAIQEEVUQxBcWwcKQ1RFIHcOC0hVQwwIXV8FCEVVRSNzbyhET0QBCgQBXFwGDgFCT0cBCBQBbyVET0IKBwFaWxFCT0QCCxYCNxQUHCMcLzgvHBwvHBstGxstNi0aIRsS/wBMAU0BUgJXAjABMwFsAWkBMgEsAk0CQwIoEyhGUC4ya200MVpUTkz8lHxAFE1OAVMCWAIwAjQBbAFpATEBLAJMAkICJxUoRU4uMmpuNDFaVVBOA2t7O/vwFiA6Q0aKOzdLDg4iLCh3QkeJOzcmJwsOEBEsExsaECUbGEhviUVBcEsbDA0jJDc5iEVBOREh/iUbEywiHEpxi0dCeCYsIg4OSTk6i4k6IBYBASASOEJFiDk3SRYbJCdvQkWJbyQlCgwbJBEBCR8cGScVCwoTFRgbQRsZJgoLFSU2IBgXGSMLCgoLEhIYGC4XGBISCgsLCiMZFhIfLS0/LCwgGSMjMiMjAbkMNyEdMh0dMjsyHcdnEUpXShATfBoaR1dMBwVoaA9KWEcRHXwnR1hKBwhpAmYECktbSCSAGxBIW0sOaWkDCE1aSBQdgBMNS1pKD2gEZxFJVEoBEhEBeAIaGQFHVEwBBgZoZxFKVUcBEhsCeClHVUoBCAdcCQ03Ih4zHR0zPDMeBB0xOjAdHTAdITUMCecoASQeARQGAwEHBgcWAR8BGCAYIBcIBgcBAwcUHyQoDw1r/O0lIBsBEQQBAwsICQEZASIBGR8aIhoKCAsDAQURGyElMRFsAqAFCRoaUDAsWikrOgoJFBkbTzAsLjElKR8cCgQQBAcvSFdaUBoZEg8wIicpLC0uTxoZCQMCEAQJO1NaXFAbGRQJCTsrKFotL1E0CQUQAgMJGRtOLi1ZRy4HCBIZG09aLC8hJC4HBMUkInaZrE5MOz0fJCQhdkxOrJl0SBEdIXBKTapNSzk3IB4eIDc5S02qTUpwIR3+uDBEMDBEMBEmNyYmNyYAABQAAP92ByIDPwDcAOsA7wDzAPcBBwEYASABKAEsAUQBTAFUAV0BZgF0AYIBigGTAaEAAAEiBgcGByYnJiIHDgEXFhcWFwYHBgcnNQcGJyYvAgYiBw4BFx4BHwEWFxYXFhczFhcWFxYXFgYHDgEHBiMGJyYvAS4BJyYHBgcGBwYHBg8CDgIWHwEyHwIyNzM2FxYXFhcWHwEWFxYXFhcWFxY2NzY/ATY3NhcWFxYHBgcGBwYHBicmLwEmJyYHIgcGBwYHBg8BFBYfARYfARYfARYXFhcWFxYXFhcWFxY3NjcvASMnFjc2NxYXHgEyNzY3NjcWFxY3PgEnJicmJzY3PgEmJyYiBwYHJicmJyYHMhcWFxYXBgcmJzY3PgEFFSE1BRUhNQUVITUFFhcWFwYHBgcmJy4BNjc2ITIXHgEGBwYHJicmJzY3NjcFFhcGBwYHNiUWFyYnJic2BRUhNQUWFxYfARYVFAcGBwYHLgEnJjU0NzY3NhcWFwYHNjU0JQYVFBcmJzY3IgYUFjI2NCYHMhYUBiImNDYFFhcWFxYOAScmJzY3NiUWFxYXBgcGLwE2PwE2FxYXFhcGByYlBgcmJzY3NjcHFhcGBw4BIicmJyYnNgWAIz8UDQkoGSE6FTIkDw8qERsXGRsSbUlSKxlEYBMDBgQCAgEBBQcDEhk3MBtBAh0OFQkUCAMEBAYgEismHiUYMRUTThYnJyM3OjQ5JAsPCQQBAgICAwEDAQogGUILTyY+Gx4ZExUREQgPDSEiGSIeMx4TKwgrEBMLCQQEAwMWGzA2NBgjFS4UKxQiGTlUJhAJBwQCAQUDCQkPFh8SEDUaLRwaIhQpPR80KUVWNWUDAgEuJzkhIgsNFT5GIB0WDQw0GEIuMCYQDSwXGBUWKSAmMBU7IBwuCQ4VHiAjHRsaGA4IPzxIPAoMFTf9iwEv/lcBLv5NAS8BWBwfHCQMBj42ERoqGh8rFwI2GhcrHxwoGhE4RgYLHSwVJP5sOj0iKCEcBAERDAMRIiQmOPycAS8B7TEiDBcZBAQdIzAfKE0kAwMgJirGQDM1PgP+uQMDPisv3CY3N003NyciLi5DLy8BQBMcKA4PIFc/IykLBUT+BjBEBA0kHz0oPA8hBxmRJB4iJEUzDAEiBQo8MyYfEB+EPEELDRg1OhscFQ0LQgM/UT8kMQoEBgQLQC8sNBUbFh4hIt4HAwIBAQYJAQECAQgBBAMEAggJFQwHBAIBAwQIEAcLAgMIAQUBAwIFAgIQAwQCAgcHCQkIAgQDAgECBQQCAQEBAQIBAQEFBQwJDwwNBgkFDAUEAQEHCAYWBBYBAQsIEhoTHBAVEBMEAgQDBwMIAgUBDwcGAwMCAwUDBAEDAQIBAgEBBAMEBgYMBxIZCRECAxYNJAYGXwYKBQo0KEFQKCVEJTkOAwsJC0AvLTMcGBUbMV5ACwQGBAwvKEMlKA0jH0YqLBMdIREyIkJGEQ4OUQ4OXA4OEwIEBAo/SCsyER4zUTcJBQUJNVUxHhE1Lj5ACQgEAhgRHA8YExM4P0QsCxQWERkSDg4OGBMHDw9EJiI+FBUbDhItGSwuMzAWFhhHKzAyKiotMicrLiknKyosJzNKNDRKMw4rPiwsPitOEyAxKyg3EgoGDDdJLjYvLkhCCwQJB3ojKAgdWRgRFBEfD0E0PjQTGBITCRNJHBU2KEREIyY/KDQSAAAC//7/agR4A1IAiwCeAAATMAcGBwYVBhcWFxYXFhcWFxYXFhcWFwYVFBYzMj4BNCc2NzY3FjMyNjU0Jic2NzY/ARYzMjY1NCc2NzY3JwYHBgcuASMiBhUPByMXMxc3Jz8HJxYXBwYHBgcmIyIGFQ8HIycVMxcOAQcmJyYnJicmJyYnJicmNTc0FQEWFwYHBgcmIzcnPwcWAggFBwETBg8YDxcVHB8jJiwrGBcCNycZKxoPIy0jKgwPJzcXFCgZIhcQFhwlNQYRFg0aAxoRGBYNJBUlNBITIh8cFxIKHQECJC4cChAWGRscDQ8BBQwTGSIaKBAJJzcSEyMfGxcSCxcFAx8XJwwVFSgqJiEzMA4XEAYSAQJuByEsISsfGSApHAkRFRkbHQ0PA1IBAgUIDxlOGElxPmBAUS00KjEeEAoHDSU0GCkwFyBNPloENyYYKg1XMkIgFBA3JhMQBwQCAxwDAwUJERI3JwEHFSApMDU4ATc1AjIyLCYbEgQCBxQRGCNDM1cENycCBhUgKTA1OQEBMQEXEgkPHC4pMkvJPW9MGUcYBQEB/gYkF1s5SxwULwIyMiwlHBEFAQAAAAADAAD/agQLA1IAAwAHAAsAABMHITcXBxE3AREhEfDwAvvxH/b2+/UC+gNS4+MG+f0X+AHx/RcC6QAAAAAO//b/agM6A1IACAARACkAQABZAHAAiACdAKkAzgD0ASMBLwE7AAABIgYUFjI2NCYHMhYUBiImNDYBJicuAicuAScmNjc2FhceBAYHBjc+AScuAScuAScuAQ4BFhcWFx4BFx4BBSInLgEnJjQ3PgE3NjIXHgEXFhQHDgEHBicyNjc2NzY0JyYnLgEiDgEHBhQXHgInBicuATc+ATc+AxceAQ4DBwYHBicWNjc+ATc+Ai4BBgcOAQcOAhYFDgEVFBYyNjQmIyInMxUXNxcHFhczFSMGBxcHJwYHFSM1JwcnNyYnIzUzNyc3FzY3Jx0BBgcnBxcHIxUzFhcHFzcXFTM1NjcXNyc2NzM1IyYnNycHJzUHMxUXNxcHFRYfATMVIxUGDwEXBycHBgcjFSM1JwcnNycmJzUjNTM3JzcXMzY3MxciBw4BFRQWMjY0JgcyFhQGIiY1NDY3NgF4IjExRDAwIhkjIzIkJAEVFyE9j5M9OlAPECQwLHlHSZN4Th8kMBgVJRkNDUs6PI5IRXRMGBklJjo8jkhFdP7zIx8bKgsMDAsqGx9FHxsqCwwMCyobHyIZMBQTCwwMCxMUMDMuKQsMDAspLtgXGTAkEA9QOj2Tj3ksMCQfTniTSUc9IkIldkRIjj04TRkZTHVESY47OkwYGQIXFRotQC0tIAwgWBw1PjQGBUdGBgoyPjYKC1gaND4yCAVFRgwyPjQIEgIQCDRBMwtHRgQIM0E0GFsOBjZBMwkGR0gGBTVANRtYVR80PDQIAwFGRgQLATI8NgEJDAFVGzQ8MgEJBEVGDTI8NAEOCwErEA8VGy5BLi4gHywsPi0aFA4B9TFEMDBEMRYkMiMjMyP+xQEHDDxYMjFgKSw9CAYZHR9YYl5XPQgDGgYqIiZbLzFWHhwYDClKLC4vMVYeHBi6JSB3S0+pT0t3ICUlIHdLT6lPS3cgJRY7NzpJTaVOSDo3OzlySU2mTUpyOXwBAwc9LSlgMTJYOxkGCD1XXmJZHh4MBxgGFx0eVTIuXEgqDBkbHlYyL1tIKxsJJxggLi5ALo9KDDU/NQkQWRURMj83BgNKSgs1PzMOE1kcMz81BQZMAkkGBDVBNBpcDxAzQTUKS0sEBDdBMxATXBAINUE1C0sESgw1PTUBCw4BVgETEgEyPTcBBgNKSgw1PTMBEw0BVh4zPDQIA0IGCSgYIS8vQi4DLT8tLSAXJwgGAAAKAAD/aQS7A1IAQQBGAEsAWgBfAGQAaQBuAHMAeAAAASIGByEVIRQWFxUhFSEVDgIHIxUzHgIyPgE3Mx4BMjY3MzUjLgEnNT4CNzM1Iy4CIg4BByE1PgE3ITUhLgEBHgEXIycVIz4BBSEeAhcVDgEHIy4CJyUzFS4BNzMOAQcFFSM+ATceARcjBzMVLgE3Mw4BBwF8JDYI/uYBFzEl/pMBbShDKgPV1QQuSFRJLgPgCDZINwj59wE0JihFKgO4uAMtSVVJLgP+yyY0AQLa/SMINwHKNEgFgRl7BUX+ZwE1AylCJyQxAd0CKkMoAU97MUWPgQVINP4EfgRHTDJIBH6XfjJHkn4FSDEDUishGSU3Bu8Z0wMqQycaKUQoKEQpIiwsIhomOARwAipEKBkqRigoRirvBDgmGSEr/sgERzN+fjJHkidDKgNyBTclJ0MqA9N+BUcyM0cEbn4ySAQESDIaewRGMTFGBAAI//X/agN7A1IACgATAC0ARABdAHUAkACoAAABIg4BFRQWMjY0JgcyFhQGIiY0NgEmJyYnLgInJicmNjc2HgIXHgEXFgYHBjc+ASYnJicuAScmJyYOARceBBcWBSInLgEnJjQ3PgE3NjIXHgEXFhQHDgEHBicyPgE3NjQnLgEnJiIGBwYHBhQXFhceASUGJy4BNzY3PgI3Njc2Fx4BBw4BBw4CBwYnFjc2Nz4BNzY3PgEmJyYHDgQHBhYBuBosGjhRODkoHikpOyoqAUUeJEdTVquNLi8QEyo4Mo+orUdEXxASKzYdGCwdHisuQkenVFFER1UdDhBXiamlREL+xSkjIDENDg4NMSAjUyMgMQ0ODg0xICMqHTkvDQ0NDS8dGzs3GRcNDQ0NFxk5/wAbHTgqExAvLo2rVlNHSjE2KxIQX0RHrahHIlQqR0RRVKdHQi4rHh0sLkJEpamJVxAOHQG4GiwaKDk5UDgaKTspKTsp/pACBw8iI2h0ODouNUYKBx1GZzo5cjA1RwgDHgYwVzQ2NztkIyEODg4yKCxqcWVEDg/ZKyaKWVvKW1mKJisrJotYXMhcWIsmKxpEhFZaxFpWhCMhQ0NCVlrEWlZCQ0ORAQMKRjUuOjh0aCMiDw4HCEc1MHI4O2dGDwYaBw4OISNkOzc2NFcwBggPDkRlcWosKDIAAAAS//X/agUVA1IAQQBRAJYApgC2AL4AxwDjAPAA+QECAQsBGgEqATIBPAFNAVwAAAEiBwYHBgcmJyYOARcWFxYXBgcGBwYWFxY3NjcWFxYXFjI3Njc2NxYXFjc+AScmJyYnNjc2NzYmJyYHBgcmJyYnJgcyFxYXFhcGByYnNjc2NzYFIgcGBwYHBgcGBwYUFx4CMj8CNjc2MzIWFx4BFRQGBwYHBisBIg4BBwYVFxQWFx4BMzI3Nj8BPgE3Njc2NTQuAScmBTYXFhcGBwYHJicuATY3NgUyFx4BBgcGByYnJic2NzYFFhcGBwYHNiUWFyYnJi8BNgcWFxYfARYVFA8BBgcGByYnJi8BJjU0NjU2NzYXIg4BFB4BMj4BNC4BFxYXBgc2NTQvATIWFAYiJjQ2BxQVBxQXJic2BxYXFhcHBgcGLgE3Njc2JRYXFhcWBwYHBicmJzY3NgUWFxYXBgcmJQYHJi8BNz4BNwUiDgIUHgIyPgI0LgIFFhcGBwYHBiInJicmJzYBrCkjIRcJDSQdRWsrEhEvGDQwHC8REis2MUkcLAcIFyEjUiMgGQkNNSJIMjcqEhEvEx4eEy8REis2MUkiMA4NGSAjKR4cHhYQCzA0YEgJDBYeHgKvECAbDxMNCwgEAgEBAgIIBgoWBRELFBcaKgwODQoNChUTGQcIEgoCAgUHBQgRExgPDAIDIzkVEw0LGDMpK/v9JzIbJxMEMCAvHSwdHSwOApMKDiweHysZFzpLBAsuJTL+T0xFMCwaMAYBJQUGHhELGAgvSiYUESAOAwYUGg4UJkMoLCoBAgIqLSgrGisaGis0KxoaK48/Mjw3BQKqHSoqOioqpQECJxcgMi4jCBMIKxRHVh0ODyweAmsXGSsQDw8PLCpHIjUJB0r+ayggOSNDSRABKQMKEyYRCBgWFAINExwPBgYQGSoZEAYGDxv9ez8gCgwWHhw6Hh4WCAdLA1IrJ0UZMAwFDxBGNS85HjAsIjkvNkUICRAFDhwVRScrKyZGGzQSBg8HCkU0LzoXHh4XOS81RggJEAYQPyhGJisYIiRBKz0QGTEXJyJBJCKLBAUFBQcFBgQJBScHDAkGBQsCBQMFDw4OJRkVJA4MCggECwoOC3IGBwEDAQUDCl4DHBgUIx0qJ0U0EBA7AgsFDFdiIx0rIjZVMgcCGQIHMVc0HhY2NDs0EAcLCxojGBsPIFYqHzcUCgcNBBUiFAwKFgkuMS1ODRIIDBQgGRoeGyMTCiMJHhsZLBotNS0bGy01LRoNLS82JzkxEiMPKz4rKz4rIQUMEhYoHBUbLCcZZFMCDAQODjMoLDUkKRYeNC0qGBoGBw4GEi4+MxkcEyERIhlELR8zBhEHBA0NDiIIEBsoGhIHBxIaKBsQCBkdCy8gQSQiIiRBGRgZAAAABQAA/2oD8wNSAAUACQANABEAFQAAGQEhNSERAREzEQURMxEFETMRFxUzNQPz/GYBPZr+gKUBHaU3pQNS/BhYA5D+4v3mAhrQ/rYBSjf+7QETpW5uAAAAAAQAAP/wA6wCzQATACcANwBHAAATIg4BFREUHgEzITI+ATURNC4BIwUhMh4BFREUDgEjISIuATURND4BFyIGFREUFjMhMjY1ETQmIwUhMhYVERQGIyEiJjURNDbSKUQoKEQpAkUoRSgoRSj9uwJFHzQfHzQf/bsfNR8fNTwmNzcmAgsmNjYm/fUCCxgiIhj99RkiIgLMKEQp/k4oRSgoRSgBsilEKCIfNR/+Th81Hx81HwGyHzUfMTYm/oImNjYmAX4mNiIiGP6CGCIiGAF+GCIAAAAAAgAA/78DiwL3ABMAHAAAAQ4DHgM3PgI3Byc/AS4BCQEGHgE3AS4BAqIsUDsdBihDVC0wVTsLnoI6piJY/u/+mwMfMRcBXR8vAvMDKEVWWlE7HQMDL00wN0uMOiAg/p3+kxYxHwMBZBI3AAAAAQAA/+IDiALxABMAAAEGBwYHFh8BBwYHFh8BNj8BFzY3A4hRULRbFCciv6pUBw4LWbKyWDFaAvEpKForEiciv6pVBw4LWrKzWGCtAAIAAP/dA4oC8wARABUAAAEGBwYHFh8BDwI/Axc2NwEPATcDilFQtFsWKxvKzyj3AiDJWDFa/igboBoC8ykoWisULBvMIvcoD8XKWGCt/nKhGqEAAAIAAP/iA4oC8QAGAAoAAAEFFwEXARcFFSE1A4r+UVv+RSABvFn9zAFqAvHWWv5BIAHAWZ4rKwAAAAASAN4AAQAAAAAAAAA1AAAAAQAAAAAAAQAEADUAAQAAAAAAAgAHADkAAQAAAAAAAwAEAEAAAQAAAAAABAAEAEQAAQAAAAAABQALAEgAAQAAAAAABgAEAFMAAQAAAAAACgArAFcAAQAAAAAACwATAIIAAwABBAkAAABqAJUAAwABBAkAAQAIAP8AAwABBAkAAgAOAQcAAwABBAkAAwAIARUAAwABBAkABAAIAR0AAwABBAkABQAWASUAAwABBAkABgAIATsAAwABBAkACgBWAUMAAwABBAkACwAmAZlDb3B5cmlnaHQgKEMpIDIwMjMgYnkgb3JpZ2luYWwgYXV0aG9ycyBAIGZvbnRlbGxvLmNvbWJwbW5SZWd1bGFyYnBtbmJwbW5WZXJzaW9uIDEuMGJwbW5HZW5lcmF0ZWQgYnkgc3ZnMnR0ZiBmcm9tIEZvbnRlbGxvIHByb2plY3QuaHR0cDovL2ZvbnRlbGxvLmNvbQBDAG8AcAB5AHIAaQBnAGgAdAAgACgAQwApACAAMgAwADIAMwAgAGIAeQAgAG8AcgBpAGcAaQBuAGEAbAAgAGEAdQB0AGgAbwByAHMAIABAACAAZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AYgBwAG0AbgBSAGUAZwB1AGwAYQByAGIAcABtAG4AYgBwAG0AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAYgBwAG0AbgBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AQIBAwEEAQUBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAWsBbAFtAW4BbwFwAXEBcgFzAXQBdQF2AXcBeAF5AXoBewF8AX0BfgF/ABpxdWFudG1lLWN1dHRpbmctc3VicHJvY2VzcwV0cmFzaB1xdWFudG1lLWRhdGEtcHJlcGFyYXRpb24tdGFzaxBnYXRld2F5LXBhcmFsbGVsH2ludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1jYW5jZWwxaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW5vbi1pbnRlcnJ1cHRpbmctbWVzc2FnZRhzdGFydC1ldmVudC1jb21wZW5zYXRpb24uc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1wYXJhbGxlbC1tdWx0aXBsZQtsb29wLW1hcmtlchJwYXJhbGxlbC1taS1tYXJrZXIjc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1zaWduYWwvaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW5vbi1pbnRlcnJ1cHRpbmctdGltZXIqaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLXBhcmFsbGVsLW11bHRpcGxlJWludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1jb21wZW5zYXRpb24LZ2F0ZXdheS14b3IQZW5kLWV2ZW50LWNhbmNlbCJpbnRlcm1lZGlhdGUtZXZlbnQtY2F0Y2gtY29uZGl0aW9uO2ludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLXBhcmFsbGVsLW11bHRpcGxlFXN0YXJ0LWV2ZW50LWNvbmRpdGlvbiJzdGFydC1ldmVudC1ub24taW50ZXJydXB0aW5nLXRpbWVyFHNlcXVlbnRpYWwtbWktbWFya2VyCXVzZXItdGFzaw1idXNpbmVzcy1ydWxlEnN1Yi1wcm9jZXNzLW1hcmtlch1zdGFydC1ldmVudC1wYXJhbGxlbC1tdWx0aXBsZRFzdGFydC1ldmVudC1lcnJvch9pbnRlcm1lZGlhdGUtZXZlbnQtY2F0Y2gtc2lnbmFsHmludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1lcnJvchZlbmQtZXZlbnQtY29tcGVuc2F0aW9uFHN1YnByb2Nlc3MtY29sbGFwc2VkE3N1YnByb2Nlc3MtZXhwYW5kZWQEdGFzaw9lbmQtZXZlbnQtZXJyb3IjaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLWVzY2FsYXRpb24eaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLXRpbWVyFnN0YXJ0LWV2ZW50LWVzY2FsYXRpb24Sc3RhcnQtZXZlbnQtc2lnbmFsEmJ1c2luZXNzLXJ1bGUtdGFzawZtYW51YWwHcmVjZWl2ZQ1jYWxsLWFjdGl2aXR5EXN0YXJ0LWV2ZW50LXRpbWVyE3N0YXJ0LWV2ZW50LW1lc3NhZ2UXaW50ZXJtZWRpYXRlLWV2ZW50LW5vbmUdaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLWxpbmsUZW5kLWV2ZW50LWVzY2FsYXRpb24HYnBtbi1pbw9nYXRld2F5LWNvbXBsZXgSZ2F0ZXdheS1ldmVudGJhc2VkDGdhdGV3YXktbm9uZQpnYXRld2F5LW9yE2VuZC1ldmVudC10ZXJtaW5hdGUQZW5kLWV2ZW50LXNpZ25hbA5lbmQtZXZlbnQtbm9uZRJlbmQtZXZlbnQtbXVsdGlwbGURZW5kLWV2ZW50LW1lc3NhZ2UOZW5kLWV2ZW50LWxpbmsgaW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW1lc3NhZ2UlaW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LWNvbXBlbnNhdGlvbhRzdGFydC1ldmVudC1tdWx0aXBsZQZzY3JpcHQLbWFudWFsLXRhc2sEc2VuZAdzZXJ2aWNlDHJlY2VpdmUtdGFzawR1c2VyEHN0YXJ0LWV2ZW50LW5vbmUjaW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LWVzY2FsYXRpb24haW50ZXJtZWRpYXRlLWV2ZW50LWNhdGNoLW11bHRpcGxlNGludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLWVzY2FsYXRpb24daW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LWxpbmsmc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1jb25kaXRpb24LZGF0YS1vYmplY3QLc2NyaXB0LXRhc2sJc2VuZC10YXNrCmRhdGEtc3RvcmUnc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1lc2NhbGF0aW9uIGludGVybWVkaWF0ZS1ldmVudC10aHJvdy1tZXNzYWdlMmludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLW11bHRpcGxlMGludGVybWVkaWF0ZS1ldmVudC1jYXRjaC1ub24taW50ZXJydXB0aW5nLXNpZ25hbCFpbnRlcm1lZGlhdGUtZXZlbnQtdGhyb3ctbXVsdGlwbGUkc3RhcnQtZXZlbnQtbm9uLWludGVycnVwdGluZy1tZXNzYWdlDWFkLWhvYy1tYXJrZXIMc2VydmljZS10YXNrCXRhc2stbm9uZRNjb21wZW5zYXRpb24tbWFya2VyJXN0YXJ0LWV2ZW50LW5vbi1pbnRlcnJ1cHRpbmctbXVsdGlwbGUfaW50ZXJtZWRpYXRlLWV2ZW50LXRocm93LXNpZ25hbDNpbnRlcm1lZGlhdGUtZXZlbnQtY2F0Y2gtbm9uLWludGVycnVwdGluZy1jb25kaXRpb24LcGFydGljaXBhbnQZZXZlbnQtc3VicHJvY2Vzcy1leHBhbmRlZBFsYW5lLWluc2VydC1iZWxvdwpzcGFjZS10b29sEGNvbm5lY3Rpb24tbXVsdGkEbGFuZQpsYXNzby10b29sEWxhbmUtaW5zZXJ0LWFib3ZlEWxhbmUtZGl2aWRlLXRocmVlD2xhbmUtZGl2aWRlLXR3bwpkYXRhLWlucHV0C2RhdGEtb3V0cHV0CWhhbmQtdG9vbAVncm91cA90ZXh0LWFubm90YXRpb24QcGxhbnFrLWRhdGEtcG9vbBxkYXRhZmxvdy10cmFuc2Zvcm1hdGlvbi10YXNrE3BsYW5xay1zZXJ2aWNlLXRhc2sXZGF0YWZsb3ctZGF0YS1zdG9yZS1tYXAYZGF0YWZsb3ctZGF0YS1tYXAtb2JqZWN0HHF1YW50bWUtaHlicmlkLXJ1bnRpbWUtZ3JvdXAXcXVhbnRtZS1ldmFsdWF0aW9uLXRhc2sqcXVhbnRtZS12YXJpYXRpb25hbC1xdWFudHVtLWFsZ29yaXRobS10YXNrGnF1YW50bWUtd2FybS1zdGFydGluZy10YXNrGXF1YW50bWUtb3B0aW1pemF0aW9uLXRhc2sdcXVhbnRtZS1vcmFjbGUtZXhwYW5zaW9uLXRhc2smcXVhbnRtZS1xdWFudHVtLWNpcmN1aXQtZXhlY3V0aW9uLXRhc2skcXVhbnRtZS1xdWFudHVtLWNpcmN1aXQtbG9hZGluZy10YXNrIHF1YW50bWUtcXVhbnR1bS1jb21wdXRhdGlvbi10YXNrLXF1YW50bWUtcXVhbnR1bS1oYXJkd2FyZS1zZWxlY3Rpb24tc3VicHJvY2VzcyVxdWFudG1lLXJlYWRvdXQtZXJyb3ItbWl0aWdhdGlvbi10YXNrC3RyYW5zYWN0aW9uDHNjcmV3LXdyZW5jaApjb25uZWN0aW9uEGNvbmRpdGlvbmFsLWZsb3cMZGVmYXVsdC1mbG93AAA=") + format("truetype"); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -23,7 +25,8 @@ } */ -[class^="bpmn-icon-"]:before, [class*=" bpmn-icon-"]:before { +[class^="bpmn-icon-"]:before, +[class*=" bpmn-icon-"]:before { font-family: "bpmn"; font-style: normal; font-weight: normal; @@ -47,7 +50,7 @@ /* you can be more comfortable with increased icons size */ /* font-size: 120%; */ - + /* Font smoothing. That was taken from TWBS */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -57,13 +60,13 @@ } @font-face { - font-family: 'bpmn'; - src: url('./fontello/font/bpmn.eot'); - src: url('./fontello/font/bpmn.eot') format('embedded-opentype'), - url('./fontello/font/bpmn.woff2') format('woff2'), - url('./fontello/font/bpmn.woff') format('woff'), - url('./fontello/font/bpmn.ttf') format('truetype'), - url('./fontello/font/bpmn.svg') format('svg'); - font-weight: normal; - font-style: normal; -} \ No newline at end of file + font-family: "bpmn"; + src: url("./fontello/font/bpmn.eot"); + src: url("./fontello/font/bpmn.eot") format("embedded-opentype"), + url("./fontello/font/bpmn.woff2") format("woff2"), + url("./fontello/font/bpmn.woff") format("woff"), + url("./fontello/font/bpmn.ttf") format("truetype"), + url("./fontello/font/bpmn.svg") format("svg"); + font-weight: normal; + font-style: normal; +} diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_buttons.css b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_buttons.css index cb1690e9..2e40f30e 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_buttons.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_buttons.css @@ -1,55 +1,55 @@ .qwm-btn { - min-width: 120px; - height: 35px; - border-radius: 3px; - font-size: 13px; - font-weight: bold; - font-stretch: normal; - font-style: normal; - line-height: normal; - letter-spacing: normal; - text-align: center; - padding-left: 15px; - padding-right: 15px; + min-width: 120px; + height: 35px; + border-radius: 3px; + font-size: 13px; + font-weight: bold; + font-stretch: normal; + font-style: normal; + line-height: normal; + letter-spacing: normal; + text-align: center; + padding-left: 15px; + padding-right: 15px; } .qwm-btn-primary { - color: #fdfdfe; - box-shadow: 0 2px 2px 0 rgba(0,0,0,0.2); - border: solid 1px var(--blue-darken-62); - background-color: var(--blue-base-65); + color: #fdfdfe; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2); + border: solid 1px var(--blue-darken-62); + background-color: var(--blue-base-65); } .qwm-btn-primary:hover { - border: solid 1px var(--blue-darken-55); - background-color: var(--blue-darken-62); + border: solid 1px var(--blue-darken-55); + background-color: var(--blue-darken-62); } .qwm-btn-primary:active { - border: solid 1px var(--blue-darken-48); - background-color: var(--blue-darken-55); + border: solid 1px var(--blue-darken-48); + background-color: var(--blue-darken-55); } .qwm-btn-primary:disabled { - box-shadow: none; - border: solid 1px var(--blue-lighten-82); - background-color: var(--blue-lighten-75); - color: rgba(253,253,254,0.8); + box-shadow: none; + border: solid 1px var(--blue-lighten-82); + background-color: var(--blue-lighten-75); + color: rgba(253, 253, 254, 0.8); } .qwm-btn-secondary { - color: var(--grey-darken-33); - box-shadow: 0 2px 2px 0 var(--color-000000-opacity-10); - border: solid 1px var(--silver-darken-80); - background-color: var(--silver-base-97); + color: var(--grey-darken-33); + box-shadow: 0 2px 2px 0 var(--color-000000-opacity-10); + border: solid 1px var(--silver-darken-80); + background-color: var(--silver-base-97); } .qwm-btn-secondary:hover { - background-color: var(--silver-darken-94); + background-color: var(--silver-darken-94); } .qwm-btn-secondary:active { - background-color: var(--silver-darken-87); + background-color: var(--silver-darken-87); } .qwm-btn-secondary:disabled { - box-shadow: none; - border: solid 1px var(--silver-darken-87); - background-color: var(--silver-base-97); - color: var(--color-535353-opacity-50); + box-shadow: none; + border: solid 1px var(--silver-darken-87); + background-color: var(--silver-base-97); + color: var(--color-535353-opacity-50); } .qwm-btn + .qwm-btn { - margin-left: 15px; + margin-left: 15px; } diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_colors.css b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_colors.css index a0fe49c6..836fc88e 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_colors.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_colors.css @@ -1,37 +1,37 @@ :root { - --blue-base-65: #4d90ff; - --blue-base-65-opacity-10: rgba(77,144,255,0.1); - --blue-darken-48: #005df7; - --blue-darken-55: #1a70ff; - --blue-darken-62: #3c85ff; - --blue-lighten-75: #80b0ff; - --blue-lighten-82: #a2c5ff; - --blue-lighten-93: #dbe9ff; - --silver-base-97: #f8f8f8; - --silver-lighten-99: #fcfcfc; - --silver-darken-80: #cdcdcd; - --silver-darken-87: #dedede; - --silver-darken-94: #efefef; - --grey-base-40: #666666; - --grey-lighten-56: #909090; - --grey-darken-23: #3b3b3b; - --grey-darken-30: #4c4c4c; - --grey-darken-33: #535353; - --red-base-62: #ff3d3d; - --red-lighten-85: #ffb3b3; - --color-000000: #000000; - --color-000000-opacity-10: rgba(0,0,0,0.1); - --color-000000-opacity-20: rgba(0,0,0,0.2); - --color-000000-opacity-30: rgba(0,0,0,0.3); - --color-444444: #444444; - --color-535353-opacity-50: rgba(83,83,83,0.5); - --color-999999: #999999; - --color-cccccc: #cccccc; - --color-dddddd: #dddddd; - --color-eeeeee: #eeeeee; - --color-f6f6f6: #f6f6f6; - --color-fafafa: #fafafa; - --color-fefefe: #fefefe; - --color-ffffff: #ffffff; - --color-ffffff-opacity-80: rgba(255,255,255,0.8); + --blue-base-65: #4d90ff; + --blue-base-65-opacity-10: rgba(77, 144, 255, 0.1); + --blue-darken-48: #005df7; + --blue-darken-55: #1a70ff; + --blue-darken-62: #3c85ff; + --blue-lighten-75: #80b0ff; + --blue-lighten-82: #a2c5ff; + --blue-lighten-93: #dbe9ff; + --silver-base-97: #f8f8f8; + --silver-lighten-99: #fcfcfc; + --silver-darken-80: #cdcdcd; + --silver-darken-87: #dedede; + --silver-darken-94: #efefef; + --grey-base-40: #666666; + --grey-lighten-56: #909090; + --grey-darken-23: #3b3b3b; + --grey-darken-30: #4c4c4c; + --grey-darken-33: #535353; + --red-base-62: #ff3d3d; + --red-lighten-85: #ffb3b3; + --color-000000: #000000; + --color-000000-opacity-10: rgba(0, 0, 0, 0.1); + --color-000000-opacity-20: rgba(0, 0, 0, 0.2); + --color-000000-opacity-30: rgba(0, 0, 0, 0.3); + --color-444444: #444444; + --color-535353-opacity-50: rgba(83, 83, 83, 0.5); + --color-999999: #999999; + --color-cccccc: #cccccc; + --color-dddddd: #dddddd; + --color-eeeeee: #eeeeee; + --color-f6f6f6: #f6f6f6; + --color-fafafa: #fafafa; + --color-fefefe: #fefefe; + --color-ffffff: #ffffff; + --color-ffffff-opacity-80: rgba(255, 255, 255, 0.8); } diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_modal.css b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_modal.css index a29a09b5..f128551c 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_modal.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/_modal.css @@ -1,147 +1,150 @@ .qwm-modal { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10000; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 12px; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10000; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; } .qwm-modal:before { - content: ''; - position: fixed; - left: 0; - top: 0; - bottom: 0; - right: 0; - background: var(--grey-base-40); - opacity: .2; - z-index: 1001; + content: ""; + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + background: var(--grey-base-40); + opacity: 0.2; + z-index: 1001; } .qwm-modal ul { - padding-left: 20px; + padding-left: 20px; } .qwm-modal table { - table-layout: fixed; - font-family: arial, sans-serif; - border-collapse: collapse; - width: 100%; + table-layout: fixed; + font-family: arial, sans-serif; + border-collapse: collapse; + width: 100%; } -.qwm-modal td, .qwm-modal th { - border: 1px solid #dddddd; - text-align: left; - padding: 8px; - word-wrap: break-word; - overflow-wrap: break-word; +.qwm-modal td, +.qwm-modal th { + border: 1px solid #dddddd; + text-align: left; + padding: 8px; + word-wrap: break-word; + overflow-wrap: break-word; } .qwm-modal tr:nth-child(even) { - background-color: #dddddd; + background-color: #dddddd; } .qwm-modal #progress { - width: 100%; - background-color: #ddd; + width: 100%; + background-color: #ddd; } .qwm-modal #bar { - width: 1%; - height: 30px; - background-color: #4CAF50; + width: 1%; + height: 30px; + background-color: #4caf50; } -.qwm-modal tr.spaceUnder>td { +.qwm-modal tr.spaceUnder > td { } -.qwm-modal html, body, #container { - height: 100%; +.qwm-modal html, +body, +#container { + height: 100%; } .qwm-modal-dialog { - position: fixed; - left: 0; - right: 0; - width: 638px; - margin: 10vh auto 6vh; - padding: 0; - z-index: 1001; - overflow: hidden; - border: solid 1px var(--silver-darken-87); - border-radius: 3px; - box-shadow: 0 1px 4px var(--color-000000-opacity-30); - background-color: var(--silver-lighten-99); - color: var(--grey-darken-33); + position: fixed; + left: 0; + right: 0; + width: 638px; + margin: 10vh auto 6vh; + padding: 0; + z-index: 1001; + overflow: hidden; + border: solid 1px var(--silver-darken-87); + border-radius: 3px; + box-shadow: 0 1px 4px var(--color-000000-opacity-30); + background-color: var(--silver-lighten-99); + color: var(--grey-darken-33); } .qwm-modal-content, .qwm-modal-content > form { - display: flex; - flex: auto; - flex-direction: column; - max-height: 84vh; + display: flex; + flex: auto; + flex-direction: column; + max-height: 84vh; } .qwm-modal-header { - padding: 20px 60px 20px 20px; - background-color: var(--silver-base-97); - font-size: 16px; - border-bottom: solid 1px var(--silver-darken-87); + padding: 20px 60px 20px 20px; + background-color: var(--silver-base-97); + font-size: 16px; + border-bottom: solid 1px var(--silver-darken-87); } .qwm-modal-title { - margin: 0; - font-size: 16px; - font-weight: bold; + margin: 0; + font-size: 16px; + font-weight: bold; } .qwm-modal-body { - padding: 16px 20px 20px 20px; - overflow-y: auto; - font-size: 14px; - background-color: var(--silver-lighten-99); + padding: 16px 20px 20px 20px; + overflow-y: auto; + font-size: 14px; + background-color: var(--silver-lighten-99); } .qwm-modal-body legend { - font-size: 14px; - font-weight: bold; - font-stretch: normal; - font-style: normal; - line-height: normal; - letter-spacing: normal; - color: var(--grey-darken-33); - margin-bottom: 23px; + font-size: 14px; + font-weight: bold; + font-stretch: normal; + font-style: normal; + line-height: normal; + letter-spacing: normal; + color: var(--grey-darken-33); + margin-bottom: 23px; } .qwm-modal-body fieldset { - border-style: none; - margin: 20px auto; - padding-left: 0; - padding-right: 0; + border-style: none; + margin: 20px auto; + padding-left: 0; + padding-right: 0; } .qwm-modal-body form { - font-size: 1.1em; + font-size: 1.1em; } .qwm-modal-body .qwm-fields { - display: grid; - grid-row-gap: 15px; + display: grid; + grid-row-gap: 15px; } .qwm-modal-footer { - padding: 15px 20px; - border-top: solid 1px var(--silver-darken-87); - text-align: right; + padding: 15px 20px; + border-top: solid 1px var(--silver-darken-87); + text-align: right; } .qwm-modal-dialog .qwm-close { - position: absolute; - top: 20px; - right: 20px; - width: 22px; - height: 22px; - padding: 0; - border: none; - border-radius: 3px; - background-color: transparent; + position: absolute; + top: 20px; + right: 20px; + width: 22px; + height: 22px; + padding: 0; + border: none; + border-radius: 3px; + background-color: transparent; } .qwm-modal-dialog .close:hover { - background-color: var(--silver-darken-94); + background-color: var(--silver-darken-94); } .qwm-modal-dialog .close:focus { - background-color: var(--silver-darken-87); + background-color: var(--silver-darken-87); } diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/style.css b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/style.css index 7c92caa5..b432a540 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/style.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/camunda-styles/style.css @@ -1,3 +1,3 @@ @import url("_colors.css"); @import url("_buttons.css"); -@import url("_modal.css"); \ No newline at end of file +@import url("_modal.css"); diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css b/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css index a6ff1594..3a96a459 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/editor-ui.css @@ -1,245 +1,224 @@ .qwm-extensible-btn, .qwm-toolbar-btn { - box-sizing: border-box; - outline: none; - background-color: transparent; - border: none; - border-radius: 6px; - font-size: 15px; - font-family: Arial, serif; - font-weight: bold; - margin: 2px; - padding: 8px; - color: #636363; + box-sizing: border-box; + outline: none; + background-color: transparent; + border: none; + border-radius: 6px; + font-size: 15px; + font-family: Arial, serif; + font-weight: bold; + margin: 2px; + padding: 8px; + color: #636363; } .qwm-properties-btn { - box-sizing: border-box; - outline: none; - background-color: transparent; - border: solid; - border-radius: 6px; - font-size: 15px; - font-family: Arial, serif; - font-weight: bold; - margin: 2px; - padding: 8px; - color: #636363; + box-sizing: border-box; + outline: none; + background-color: transparent; + border: solid; + border-radius: 6px; + font-size: 15px; + font-family: Arial, serif; + font-weight: bold; + margin: 2px; + padding: 8px; + color: #636363; } .qwm-shortcuts { - right: 0px; - position: absolute; - box-sizing: border-box; - outline: none; - background-color: transparent; - border: none; - border-radius: 6px; - font-size: 15px; - font-family: Arial, serif; - font-weight: bold; - margin: 2px; - padding: 8px; - color: #636363; + right: 0px; + position: absolute; + box-sizing: border-box; + outline: none; + background-color: transparent; + border: none; + border-radius: 6px; + font-size: 15px; + font-family: Arial, serif; + font-weight: bold; + margin: 2px; + padding: 8px; + color: #636363; } .qwm-shortcuts:hover { - background-color: #f7f7f9; + background-color: #f7f7f9; } .qwm-shortcuts:active { - background-color: #e8e8e8; + background-color: #e8e8e8; } .qwm-toolbar-btn:hover { - background-color: #f7f7f9; + background-color: #f7f7f9; } .qwm-toolbar-btn:active { - background-color: #e8e8e8; + background-color: #e8e8e8; } .qwm-toolbar-transformation-btn { - background-color: transparent; - border: none; - border-radius: 6px; + background-color: transparent; + border: none; + border-radius: 6px; } .qwm-toolbar-transformation-btn:hover { - background-color: #e8e8e8; + background-color: #e8e8e8; } .qwm-toolbar-transformation-btn:active { - background-color: #e8e8e8; + background-color: #e8e8e8; } .qwm-toolbar-splitter { - margin: 5px; - border: 1px solid #cccccc; - max-height: 100%; + margin: 5px; + border: 1px solid #cccccc; + max-height: 100%; } .qwm-toolbar { - background: #ffffff; - display: flex; - /*align-items: flex-start;*/ - overflow: auto; + background: #ffffff; + display: flex; + /*align-items: flex-start;*/ + overflow: auto; } .qwm-extensible-btn { - background-color: #e8e8e8; + background-color: #e8e8e8; } .qwm-extensible-buttons-list { - background: #ffffff; - position: absolute; - display: flex; - padding: 4px; - border: 2px solid #e8e8e8; - border-radius: 6px; - z-index: 9999; - overflow: auto; + background: #ffffff; + position: absolute; + display: flex; + padding: 4px; + border: 2px solid #e8e8e8; + border-radius: 6px; + z-index: 9999; + overflow: auto; } .qwm-icon-saving:before { - content: ""; - width: 15px; - height: 15px; - background-size: contain; - background-image: url("../icons/save-outline-icon.png"); - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 15px; + height: 15px; + background-size: contain; + background-image: url("../icons/save-outline-icon.png"); + background-repeat: no-repeat; + display: inline-block; + float: left; } .qwm-icon-open-file:before { - content: ""; - width: 20px; - height: 15px; - background-size: contain; - background-image: url("../icons/open-folder-outline-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 20px; + height: 15px; + background-size: contain; + background-image: url("../icons/open-folder-outline-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; } .qwm-icon-new-file:before { - content: ""; - width: 15px; - height: 15px; - background-size: contain; - background-image: url("../icons/blank-file-outline-icon.png"); - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 15px; + height: 15px; + background-size: contain; + background-image: url("../icons/blank-file-outline-icon.png"); + background-repeat: no-repeat; + display: inline-block; + float: left; } .qwm-icon-shortcut:before { - content: ""; - width: 15px; - height: 20px; - margin-top: 3px; - background-size: contain; - background-image: url("../icons/shortcut.png"); - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 15px; + height: 20px; + margin-top: 3px; + background-size: contain; + background-image: url("../icons/shortcut.png"); + background-repeat: no-repeat; + display: inline-block; + float: left; } .qwm-toolbar-transformation-edit-icon { - border-color: #cccccc; - content: ""; - width: 17px; - height: 17px; - background-size: contain; - background-image: url("../icons/wrench-icon-outline.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin-left: 3px; - margin-top: 2px; + border-color: #cccccc; + content: ""; + width: 17px; + height: 17px; + background-size: contain; + background-image: url("../icons/wrench-icon-outline.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin-left: 3px; + margin-top: 2px; } .qwm-workflow-deployment-btn:before { - content: ""; - display: block; - width: 15px; - height: 15px; - background-size: contain; - background: url("../icons/workflow-deployment-icon.png") no-repeat center center; - float: left; + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../icons/workflow-deployment-icon.png") no-repeat center + center; + float: left; } .qwm-workflow-transformation-btn:before { - content: ""; - display: block; - width: 15px; - height: 15px; - background-size: contain; - background: url("../icons/worklow-transformation-icon.png") no-repeat center center; - float: left; + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../icons/worklow-transformation-icon.png") no-repeat center + center; + float: left; } .qwm-popup-menu-more-options:after { - content: ""; - background-size: contain; - background-repeat: no-repeat; - display: inline-block; - background-image: url("../icons/line-angle-right-icon.svg"); - width: 16px; - height: 16px; - float: left; - margin-left: 6px; - margin-top: 2px; + content: ""; + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + background-image: url("../icons/line-angle-right-icon.svg"); + width: 16px; + height: 16px; + float: left; + margin-left: 6px; + margin-top: 2px; } .qwm-popup-menu-less-options:before { - content: ""; - width: 23px; - height: 15px; - background-size: contain; - background-image: url("../icons/line-angle-left-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin-top: 2px; + content: ""; + width: 23px; + height: 15px; + background-size: contain; + background-image: url("../icons/line-angle-left-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin-top: 2px; } .qwm-tmp { - background: url("../icons/save-outline-icon.png") no-repeat center center; -} - -#editor { - position: absolute; - width: 100%; - height: 100%; -} - -.ace_print-margin { - display: none; + background: url("../icons/save-outline-icon.png") no-repeat center center; } .qwm-icon-upload:before { - content: ""; - width: 15px; - height: 18px; - margin-top: 0px; - background-size: contain; - background-image: url("../icons/upload-icon.png"); - background-repeat: no-repeat; - display: inline-block; - float: left; -} - -.qwm-icon-xml-viewer:before { - content: ""; - width: 20px; - height: 18px; - margin-top: 0px; - background-size: contain; - background-image: url("../icons/xml-viewer-icon.png"); - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 15px; + height: 18px; + margin-top: 0px; + background-size: contain; + background-image: url("../icons/upload-icon.png"); + background-repeat: no-repeat; + display: inline-block; + float: left; } - diff --git a/components/bpmn-q/modeler-component/editor/resources/styling/modeler.css b/components/bpmn-q/modeler-component/editor/resources/styling/modeler.css index 92f15955..db064a43 100644 --- a/components/bpmn-q/modeler-component/editor/resources/styling/modeler.css +++ b/components/bpmn-q/modeler-component/editor/resources/styling/modeler.css @@ -1,150 +1,149 @@ .qwm-content, -.qwm-content>div { - width: 100%; - height: 100%; - overflow: hidden; +.qwm-content > div { + width: 100%; + height: 100%; + overflow: hidden; } -.qwm-content>.qwm-message { - text-align: center; - display: table; +.qwm-content > .qwm-message { + text-align: center; + display: table; - font-size: 16px; - color: #111; + font-size: 16px; + color: #111; } -.qwm-content>.qwm-message .qwm-note { - vertical-align: middle; - text-align: center; - display: table-cell; +.qwm-content > .qwm-message .qwm-note { + vertical-align: middle; + text-align: center; + display: table-cell; } .qwm-content .qwm-error .qwm-details { - max-width: 500px; - font-size: 12px; - margin: 20px auto; - text-align: left; + max-width: 500px; + font-size: 12px; + margin: 20px auto; + text-align: left; } .qwm-content .qwm-error pre { - border: solid 1px #CCC; - background: #EEE; - padding: 10px; + border: solid 1px #ccc; + background: #eee; + padding: 10px; } .qwm-content:not(.with-error) .qwm-error, .qwm-content.with-error .qwm-intro, .qwm-content.with-diagram .qwm-intro { - display: none; + display: none; } - .qwm-content .qwm-canvas, .qwm-content.with-error .qwm-canvas { - visibility: hidden; + visibility: hidden; } .qwm-content.with-diagram .qwm-canvas { - visibility: visible; + visibility: visible; } .qwm-buttons { - position: fixed; - bottom: 20px; - left: 20px; + position: fixed; + bottom: 20px; + left: 20px; - padding: 0; - margin: 0; - list-style: none; + padding: 0; + margin: 0; + list-style: none; } -.qwm-buttons>li { - display: inline-block; - margin-right: 10px; +.qwm-buttons > li { + display: inline-block; + margin-right: 10px; } -.qwm-buttons>li>a { - background: #DDD; - border: solid 1px #666; - display: inline-block; - padding: 5px; +.qwm-buttons > li > a { + background: #ddd; + border: solid 1px #666; + display: inline-block; + padding: 5px; } .qwm-buttons a { - opacity: 0.3; + opacity: 0.3; } .qwm-buttons a.active { - opacity: 1.0; + opacity: 1; } .bjs-breadcrumbs { - position: absolute; - display: none; - flex-wrap: wrap; - align-items: center; - top: 20px; - left: 20px; - padding: 0px; - margin: 0px; - font-size: 20px; - line-height: normal; + position: absolute; + display: none; + flex-wrap: wrap; + align-items: center; + top: 20px; + left: 20px; + padding: 0px; + margin: 0px; + font-size: 20px; + line-height: normal; } .bjs-breadcrumbs-shown .bjs-breadcrumbs { - display: flex; + display: flex; } .djs-palette-shown .bjs-breadcrumbs { - left: 90px; + left: 90px; } .djs-palette-shown.djs-palette-two-column .bjs-breadcrumbs { - left: 140px; + left: 140px; } .bjs-breadcrumbs li { - display: inline-flex; - padding-bottom: 5px; - font-size: 17px; - font-family: Arial, serif; - font-weight: bold; + display: inline-flex; + padding-bottom: 5px; + font-size: 17px; + font-family: Arial, serif; + font-weight: bold; } .bjs-breadcrumbs li a { - cursor: pointer; - color: blue + cursor: pointer; + color: blue; } .bjs-breadcrumbs li:last-of-type a { - color: inherit; - cursor: default; + color: inherit; + cursor: default; } .bjs-breadcrumbs li:not(:first-child)::before { - content: url('data:image/svg+xml;utf8,'); - padding: 0 8px; - color: var(--breadcrumbs-arrow-color); - height: 1em; + content: url('data:image/svg+xml;utf8,'); + padding: 0 8px; + color: var(--breadcrumbs-arrow-color); + height: 1em; } .bjs-breadcrumbs .bjs-crumb { - display: inline-block; - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + display: inline-block; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .bjs-drilldown-empty { - display: none; + display: none; } .selected .bjs-drilldown-empty { - display: inherit; + display: inherit; } .bjs-drilldown { - position: relative; - top: 10px; -} \ No newline at end of file + position: relative; + top: 10px; +} diff --git a/components/bpmn-q/modeler-component/editor/rules/ModelerRules.js b/components/bpmn-q/modeler-component/editor/rules/ModelerRules.js index 7be4e38b..c2b1c40b 100644 --- a/components/bpmn-q/modeler-component/editor/rules/ModelerRules.js +++ b/components/bpmn-q/modeler-component/editor/rules/ModelerRules.js @@ -1,21 +1,17 @@ -import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules'; -import { getModeler } from '../ModelerHandler'; -import { saveModelerAsLocalFile } from '../util/IoUtilities'; +import BpmnRules from "bpmn-js/lib/features/rules/BpmnRules"; +import { getModeler } from "../ModelerHandler"; +import { saveModelerAsLocalFile } from "../util/IoUtilities"; /** * Contains the rules for the modeler. */ export default class ModelerRules extends BpmnRules { - - constructor(eventBus) { - super(eventBus); - eventBus.on('saveFile', function(context) { - saveModelerAsLocalFile(getModeler()); - }); - } + constructor(eventBus) { + super(eventBus); + eventBus.on("saveFile", function () { + saveModelerAsLocalFile(getModeler()); + }); + } } -ModelerRules.$inject = [ - 'eventBus', -]; - +ModelerRules.$inject = ["eventBus"]; diff --git a/components/bpmn-q/modeler-component/editor/rules/index.js b/components/bpmn-q/modeler-component/editor/rules/index.js index 2fc8f2d1..7fe33779 100644 --- a/components/bpmn-q/modeler-component/editor/rules/index.js +++ b/components/bpmn-q/modeler-component/editor/rules/index.js @@ -1,6 +1,6 @@ -import ModelerRules from './ModelerRules'; +import ModelerRules from "./ModelerRules"; export default { - __init__: ['modelerRules'], - modelerRules: ["type", ModelerRules] -}; \ No newline at end of file + __init__: ["modelerRules"], + modelerRules: ["type", ModelerRules], +}; diff --git a/components/bpmn-q/modeler-component/editor/shortcut/ShortcutModal.js b/components/bpmn-q/modeler-component/editor/shortcut/ShortcutModal.js index a23bde56..684dba6d 100644 --- a/components/bpmn-q/modeler-component/editor/shortcut/ShortcutModal.js +++ b/components/bpmn-q/modeler-component/editor/shortcut/ShortcutModal.js @@ -10,9 +10,9 @@ */ /* eslint-disable no-unused-vars */ -import React from 'react'; -import Modal from '../ui/modal/Modal'; -import '../config/config-modal.css'; +import React from "react"; +import Modal from "../ui/modal/Modal"; +import "../config/config-modal.css"; // polyfill upcoming structural components const Title = Modal.Title || (({ children }) =>

{children}

); @@ -26,66 +26,88 @@ const Body = Modal.Body || (({ children }) =>
{children}
); * @constructor */ export default function ShortcutModal({ onClose }) { + return ( + + Keyboard Shortcuts - return - - Keyboard Shortcuts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select All⇧ + A
Searchctrl + F
Downloadctrl + S
Redoctrl + Y
Undoctrl + Z
Scrolling (Vertical)ctrl + Scrolling
Scrolling (Horizontal)ctrl + ⇧ + Scrolling
DeleteD, Backspace
Hand ToolH
Lasso ToolL
Replace ToolR
Space ToolS
- -
; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Select All + ⇧ + A +
Search + ctrl + F +
Download + ctrl + S +
Redo + ctrl + Y +
Undo + ctrl + Z +
Scrolling (Vertical) + ctrl + Scrolling +
Scrolling (Horizontal) + ctrl + ⇧ + Scrolling +
Delete + D, Backspace +
Hand Tool + H +
Lasso Tool + L +
Replace Tool + R +
Space Tool + S +
+ +
+ ); } - diff --git a/components/bpmn-q/modeler-component/editor/shortcut/ShortcutPlugin.js b/components/bpmn-q/modeler-component/editor/shortcut/ShortcutPlugin.js index d1bc1266..f0aca3e8 100644 --- a/components/bpmn-q/modeler-component/editor/shortcut/ShortcutPlugin.js +++ b/components/bpmn-q/modeler-component/editor/shortcut/ShortcutPlugin.js @@ -9,42 +9,46 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, {PureComponent, Fragment} from 'react'; +import React, { PureComponent, Fragment } from "react"; import ShortcutModal from "../shortcut/ShortcutModal"; export default class ShortcutPlugin extends PureComponent { - - constructor(props) { - super(props); - - this.state = { - shortcutOpen: false, - }; - - this.handleConfigClosed = this.handleConfigClosed.bind(this); - } - - // callback function to close the shortcut modal - handleConfigClosed() { - this.setState({shortcutOpen: false}); - } - - render() { - - // render button and pop-up menu - return ( -
- -
- {this.state.shortcutOpen && ( - - )} -
); - } + constructor(props) { + super(props); + + this.state = { + shortcutOpen: false, + }; + + this.handleConfigClosed = this.handleConfigClosed.bind(this); + } + + // callback function to close the shortcut modal + handleConfigClosed() { + this.setState({ shortcutOpen: false }); + } + + render() { + // render button and pop-up menu + return ( + +
+ +
+ {this.state.shortcutOpen && ( + + )} +
+ ); + } } diff --git a/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js b/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js index 2e2ac26b..3e0e0289 100644 --- a/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js +++ b/components/bpmn-q/modeler-component/editor/ui/ButtonToolbar.js @@ -1,4 +1,4 @@ -import React, {Fragment} from 'react'; +import React, { Fragment } from "react"; import SaveButton from "./SaveButton"; import OpenButton from "./OpenButton"; import NewDiagramButton from "./NewDiagramButton"; @@ -17,37 +17,34 @@ import XMLViewerButton from "./XMLViewerButton"; * @constructor */ export default function ButtonToolbar(props) { + const { modeler, pluginButtons, transformButtons } = props; - const { - modeler, - pluginButtons, - transformButtons, - } = props; + const hasTransformations = transformButtons.length > 0; - const hasTransformations = transformButtons.length > 0; - - return ( - -
-
- - - - - -
- -
- {hasTransformations && - } - -
- {React.Children.toArray(pluginButtons)} - -
-
- ); -} \ No newline at end of file + return ( + +
+
+ + + + + +
+ +
+ {hasTransformations && ( + + )} + +
+ {React.Children.toArray(pluginButtons)} + +
+
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/ConfirmationModal.js b/components/bpmn-q/modeler-component/editor/ui/ConfirmationModal.js new file mode 100644 index 00000000..4269e448 --- /dev/null +++ b/components/bpmn-q/modeler-component/editor/ui/ConfirmationModal.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2023 Institute of Architecture of Application Systems - + * University of Stuttgart + * + * This program and the accompanying materials are made available under the + * terms the Apache Software License 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: Apache-2.0 + */ +import React from "react"; +import Modal from "../ui/modal/Modal"; + +// polyfill upcoming structural components +const Title = Modal.Title || (({ children }) =>

{children}

); +const Body = Modal.Body || (({ children }) =>
{children}
); +const Footer = Modal.Footer || (({ children }) =>
{children}
); + +/** + * Modal component for confirming the discard of changes in the editor. + * + * @param onClose Function called when the modal is closed. + * @param onConfirm Function called when the "Yes" button is clicked to confirm discarding changes. + * @returns {JSX.Element} The modal as a React component. + * @constructor + */ +export default function ConfirmationModal({ onClose, onConfirm }) { + return ( + + Confirm Discard Changes + + There are unsaved changes. Are you sure you want to discard all changes + and generate a new diagram? + +
+
+ + +
+
+
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js index 9180be11..1e2b8599 100644 --- a/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/DeploymentButton.js @@ -1,15 +1,16 @@ -import React, {Fragment, useState} from 'react'; -import NotificationHandler from './notifications/NotificationHandler'; -import {deployWorkflowToCamunda} from '../util/IoUtilities'; -import {getCamundaEndpoint} from '../config/EditorConfigManager'; -import {getRootProcess} from '../util/ModellingUtilities'; -import {getServiceTasksToDeploy} from '../../extensions/opentosca/deployment/DeploymentUtils'; -import {getModeler} from '../ModelerHandler'; -import OnDemandDeploymentModal from './OnDemandDeploymentModal'; -import {startOnDemandReplacementProcess} from "../../extensions/opentosca/replacement/OnDemandTransformator"; +import React, { Fragment, useState } from "react"; +import NotificationHandler from "./notifications/NotificationHandler"; +import { deployWorkflowToCamunda } from "../util/IoUtilities"; +import { getCamundaEndpoint } from "../config/EditorConfigManager"; +import { getRootProcess } from "../util/ModellingUtilities"; +import { getServiceTasksToDeploy } from "../../extensions/opentosca/deployment/DeploymentUtils"; +import { getModeler } from "../ModelerHandler"; +import OnDemandDeploymentModal from "./OnDemandDeploymentModal"; +import { startOnDemandReplacementProcess } from "../../extensions/opentosca/replacement/OnDemandTransformator"; +// eslint-disable-next-line no-unused-vars const defaultState = { - windowOpenOnDemandDeployment: false, + windowOpenOnDemandDeployment: false, }; /** @@ -20,92 +21,102 @@ const defaultState = { * @constructor */ export default function DeploymentButton(props) { - const [windowOpenOnDemandDeployment, setWindowOpenOnDemandDeployment] = useState(false); + const [windowOpenOnDemandDeployment, setWindowOpenOnDemandDeployment] = + useState(false); - const {modeler} = props; - - - /** - * Handle result of the on demand deployment dialog - * - * @param result the result from the dialog - */ - async function handleOnDemandDeployment(result) { - if (result && result.hasOwnProperty('onDemand')) { - // get XML of the current workflow - let xml = (await modeler.saveXML({format: true})).xml; - console.log("XML", xml) - if (result.onDemand === true) { - xml = await startOnDemandReplacementProcess(xml); - } - // deploy in any case - deploy(xml); - } - // handle cancellation (don't deploy) - setWindowOpenOnDemandDeployment(false); + const { modeler } = props; + /** + * Handle result of the on demand deployment dialog + * + * @param result the result from the dialog + */ + async function handleOnDemandDeployment(result) { + if (result && result.hasOwnProperty("onDemand")) { + // get XML of the current workflow + let xml = (await modeler.saveXML({ format: true })).xml; + console.log("XML", xml); + if (result.onDemand === true) { + xml = await startOnDemandReplacementProcess(xml); + } + // deploy in any case + deploy(xml); } + // handle cancellation (don't deploy) + setWindowOpenOnDemandDeployment(false); + } - /** - * Deploy the current workflow to the Camunda engine - */ - async function deploy(xml) { - - NotificationHandler.getInstance().displayNotification({ - title: 'Deployment started', - content: 'Deployment of the current Workflow to the Camunda Engine under ' + getCamundaEndpoint() + ' started.', - }); + /** + * Deploy the current workflow to the Camunda engine + */ + async function deploy(xml) { + NotificationHandler.getInstance().displayNotification({ + title: "Deployment started", + content: + "Deployment of the current Workflow to the Camunda Engine under " + + getCamundaEndpoint() + + " started.", + }); - // get XML of the current workflow - const rootElement = getRootProcess(modeler.getDefinitions()); + // get XML of the current workflow + const rootElement = getRootProcess(modeler.getDefinitions()); - // check if there are views defined for the modeler and include them in the deployment - let viewsDict = {}; - if (modeler.views !== undefined) { - console.log('Adding additional views during deployment: ', modeler.views); - viewsDict = modeler.views; - } + // check if there are views defined for the modeler and include them in the deployment + let viewsDict = {}; + if (modeler.views !== undefined) { + console.log("Adding additional views during deployment: ", modeler.views); + viewsDict = modeler.views; + } - // start deployment of workflow and views - let result = await deployWorkflowToCamunda(rootElement.id, xml, viewsDict); + // start deployment of workflow and views + let result = await deployWorkflowToCamunda(rootElement.id, xml, viewsDict); - if (result.status === 'failed') { - NotificationHandler.getInstance().displayNotification({ - type: 'error', - title: 'Unable to deploy workflow', - content: 'Workflow deployment failed. Please check the configured Camunda engine endpoint!', - duration: 20000 - }); - } else { - NotificationHandler.getInstance().displayNotification({ - type: 'info', - title: 'Workflow successfully deployed', - content: 'Workflow successfully deployed under deployment Id: ' + result.deployedProcessDefinition.deploymentId, - duration: 20000 - }); - } + if (result.status === "failed") { + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Unable to deploy workflow", + content: + "Workflow deployment failed. Please check the configured Camunda engine endpoint!", + duration: 20000, + }); + } else { + NotificationHandler.getInstance().displayNotification({ + type: "info", + title: "Workflow successfully deployed", + content: + "Workflow successfully deployed under deployment Id: " + + result.deployedProcessDefinition.deploymentId, + duration: 20000, + }); } + } - async function onClick() { - let csarsToDeploy = getServiceTasksToDeploy(getRootProcess(getModeler().getDefinitions())); - if (csarsToDeploy.length > 0) { - setWindowOpenOnDemandDeployment(true); - } else { - deploy((await modeler.saveXML({format: true})).xml); - } + async function onClick() { + let csarsToDeploy = getServiceTasksToDeploy( + getRootProcess(getModeler().getDefinitions()) + ); + if (csarsToDeploy.length > 0) { + setWindowOpenOnDemandDeployment(true); + } else { + deploy((await modeler.saveXML({ format: true })).xml); } + } - return ( - - - {windowOpenOnDemandDeployment && ( - handleOnDemandDeployment(e)} - /> - )} - - ); -} \ No newline at end of file + return ( + + + {windowOpenOnDemandDeployment && ( + handleOnDemandDeployment(e)} /> + )} + + ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/ExtensibleButton.js b/components/bpmn-q/modeler-component/editor/ui/ExtensibleButton.js index c4e2f08f..7d3f2eaa 100644 --- a/components/bpmn-q/modeler-component/editor/ui/ExtensibleButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/ExtensibleButton.js @@ -1,82 +1,91 @@ -import React, {Component, createRef, useState} from 'react'; +import React, { Component, createRef } from "react"; /** * React component defining a button which displays a list of buttons, the sub buttons under the button if the user clicks on it. Can be used * to group several button into one. */ export default class ExtensibleButton extends Component { - constructor(props) { - super(props); + constructor(props) { + super(props); - const { - subButtons, // array of buttons which are grouped into this button - title, - styleClass, - description, - } = props; + const { + subButtons, // array of buttons which are grouped into this button + title, + styleClass, + description, + } = props; - this.state = { - isToggleOn: false, - subButtons: subButtons, - title: title, - styleClass: styleClass || '', - description: description, - }; - - this.handleClick = this.handleClick.bind(this); - this.openingListener = this.openingListener.bind(this); - - this.wrapperRef = createRef(); - } - - componentDidMount() { - document.addEventListener('new-extensible-button-opened', this.openingListener); - } + this.state = { + isToggleOn: false, + subButtons: subButtons, + title: title, + styleClass: styleClass || "", + description: description, + }; - componentWillUnmount() { - document.removeEventListener('new-extensible-button-opened', this.openingListener); - } + this.handleClick = this.handleClick.bind(this); + this.openingListener = this.openingListener.bind(this); - // open or close sub buttons - handleClick() { + this.wrapperRef = createRef(); + } - if (!this.state.isToggleOn) { + componentDidMount() { + document.addEventListener( + "new-extensible-button-opened", + this.openingListener + ); + } - // dispatch event to close other extensible buttons - const newEvent = new CustomEvent('new-extensible-button-opened', { - detail: { - openButtonId: this.state.title + this.state.styleClass, - }, - }); - return document.dispatchEvent(newEvent); - } + componentWillUnmount() { + document.removeEventListener( + "new-extensible-button-opened", + this.openingListener + ); + } - this.setState(state => ({ - isToggleOn: !state.isToggleOn, - })); + // open or close sub buttons + handleClick() { + if (!this.state.isToggleOn) { + // dispatch event to close other extensible buttons + const newEvent = new CustomEvent("new-extensible-button-opened", { + detail: { + openButtonId: this.state.title + this.state.styleClass, + }, + }); + return document.dispatchEvent(newEvent); } - // callback for a listener to close this button if another extensible button is opening - openingListener = (event) => { - const currentId = this.state.title + this.state.styleClass; - this.setState({ isToggleOn: currentId === event.detail.openButtonId}); - }; + this.setState((state) => ({ + isToggleOn: !state.isToggleOn, + })); + } - render() { - return ( -
- - {this.state.isToggleOn && -
- {React.Children.toArray(this.state.subButtons)} -
- } -
- ); - } -} \ No newline at end of file + // callback for a listener to close this button if another extensible button is opening + openingListener = (event) => { + const currentId = this.state.title + this.state.styleClass; + this.setState({ isToggleOn: currentId === event.detail.openButtonId }); + }; + + render() { + return ( +
+ + {this.state.isToggleOn && ( +
+ {React.Children.toArray(this.state.subButtons)} +
+ )} +
+ ); + } +} diff --git a/components/bpmn-q/modeler-component/editor/ui/NewDiagramButton.js b/components/bpmn-q/modeler-component/editor/ui/NewDiagramButton.js index 7c4354d1..097b4bbf 100644 --- a/components/bpmn-q/modeler-component/editor/ui/NewDiagramButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/NewDiagramButton.js @@ -1,22 +1,59 @@ -import React from 'react'; -import {createNewDiagram} from '../util/IoUtilities'; +import React, { useState } from "react"; +import { createNewDiagram } from "../util/IoUtilities"; +import { getModeler } from "../ModelerHandler"; +import ConfirmationModal from "./ConfirmationModal"; -/** - * React button which creates a new workflow. - * - * @param props - * @returns {JSX.Element} - * @constructor - */ export default function NewDiagramButton(props) { + const { modeler } = props; + const [, setUnsavedChanges] = useState(false); + const [showConfirmationModal, setShowConfirmationModal] = useState(false); - const {modeler} = props; + const checkUnsavedChanges = () => { + getModeler().saveXML({ format: true }, function (err, xml) { + if (!err) { + let oldXml = getModeler().oldXml; + if (oldXml !== undefined) { + oldXml = oldXml.trim(); + } + if (oldXml !== xml.trim() && oldXml !== undefined) { + setUnsavedChanges(true); + setShowConfirmationModal(true); + } else { + setShowConfirmationModal(false); + createNewDiagram(modeler); + } + } + }); + }; - return ( - - ); -} \ No newline at end of file + const handleConfirmDiscard = () => { + createNewDiagram(modeler); + setUnsavedChanges(false); + setShowConfirmationModal(false); + }; + + const handleCancelDiscard = () => { + setShowConfirmationModal(false); + }; + + return ( +
+ + + {showConfirmationModal && ( + + )} +
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js index d7ded0da..aee49242 100644 --- a/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js +++ b/components/bpmn-q/modeler-component/editor/ui/OnDemandDeploymentModal.js @@ -10,35 +10,47 @@ */ /* eslint-disable no-unused-vars */ -import React from 'react'; +import React from "react"; // polyfill upcoming structural components -import Modal from './modal/Modal'; +import Modal from "./modal/Modal"; -const Title = Modal.Title || (({children}) =>

{children}

); -const Body = Modal.Body || (({children}) =>
{children}
); -const Footer = Modal.Footer || (({children}) =>
{children}
); +const Title = Modal.Title || (({ children }) =>

{children}

); +const Body = Modal.Body || (({ children }) =>
{children}
); +const Footer = Modal.Footer || (({ children }) =>
{children}
); -export default function OnDemandDeploymentModal({onClose}) { - - const onOnDemand = (value) => onClose({ - onDemand: value, +export default function OnDemandDeploymentModal({ onClose }) { + const onOnDemand = (value) => + onClose({ + onDemand: value, }); - return - - - Workflow Deployment - - - The current workflow contains service task with attached deployment models which support on-demand service deployment. - Would you like to use on-demand service deployment? - -
-
- - -
-
-
; + return ( + + Workflow Deployment + + The current workflow contains service task with attached deployment + models which support on-demand service deployment. Would you like to use + on-demand service deployment? + +
+
+ + +
+
+
+ ); } diff --git a/components/bpmn-q/modeler-component/editor/ui/OpenButton.js b/components/bpmn-q/modeler-component/editor/ui/OpenButton.js index 3ceb0cd9..1a207d2b 100644 --- a/components/bpmn-q/modeler-component/editor/ui/OpenButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/OpenButton.js @@ -1,9 +1,9 @@ -import React, {useRef} from 'react'; -import {loadDiagram} from '../util/IoUtilities'; -import {getModeler} from '../ModelerHandler'; -import * as editorConfig from '../config/EditorConfigManager'; -import {dispatchWorkflowEvent} from '../events/EditorEventHandler'; -import {workflowEventTypes} from '../EditorConstants'; +import React, { useRef } from "react"; +import { loadDiagram } from "../util/IoUtilities"; +import { getModeler } from "../ModelerHandler"; +import * as editorConfig from "../config/EditorConfigManager"; +import { dispatchWorkflowEvent } from "../events/EditorEventHandler"; +import { workflowEventTypes } from "../EditorConstants"; import NotificationHandler from "./notifications/NotificationHandler"; /** @@ -13,64 +13,69 @@ import NotificationHandler from "./notifications/NotificationHandler"; * @constructor */ export default function OpenButton() { + const inputRef = useRef(null); - const inputRef = useRef(null); + function handleClick() { + inputRef.current.click(); + } - function handleClick() { - inputRef.current.click(); - } - - function handleChange(event) { - - const file = event.target.files[0]; - - if (file.name.endsWith('.bpmn')) { - - // open file and load its content as bpmn diagram in the modeler - const reader = new FileReader(); - reader.onload = (e) => { + function handleChange(event) { + const file = event.target.files[0]; - const xml = e.target.result; + if (file.name.endsWith(".bpmn")) { + // open file and load its content as bpmn diagram in the modeler + const reader = new FileReader(); + reader.onload = (e) => { + const xml = e.target.result; - loadDiagram(xml, getModeler(), false).then((result) => { - // save file name in editor configs - editorConfig.setFileName(file.name); + loadDiagram(xml, getModeler(), false).then((result) => { + // save file name in editor configs + editorConfig.setFileName(file.name); - dispatchWorkflowEvent(workflowEventTypes.LOADED, xml, file.name); + dispatchWorkflowEvent(workflowEventTypes.LOADED, xml, file.name); - if (result.warnings && result.warnings.some(warning => warning.error)) { - NotificationHandler.getInstance().displayNotification({ - type: 'warning', - title: 'Loaded Diagram contains Problems', - content: `The diagram could not be properly loaded. Maybe it contains modelling elements which are not supported be the currently active plugins.`, - duration: 20000 - }); - } + if ( + result.warnings && + result.warnings.some((warning) => warning.error) + ) { + NotificationHandler.getInstance().displayNotification({ + type: "warning", + title: "Loaded Diagram contains Problems", + content: `The diagram could not be properly loaded. Maybe it contains modelling elements which are not supported be the currently active plugins.`, + duration: 20000, + }); + } - if (result.error) { - NotificationHandler.getInstance().displayNotification({ - type: 'warning', - title: 'Unable to load Diagram', - content: `During the loading of the diagram some errors occurred: ${result.error}`, - duration: 20000 - }); - } - }); - }; - reader.readAsText(file); - } + if (result.error) { + NotificationHandler.getInstance().displayNotification({ + type: "warning", + title: "Unable to load Diagram", + content: `During the loading of the diagram some errors occurred: ${result.error}`, + duration: 20000, + }); + } + }); + }; + reader.readAsText(file); } + } - return ( - <> - handleChange(event)}/> - - - ); -} \ No newline at end of file + return ( + <> + handleChange(event)} + /> + + + ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/SaveButton.js b/components/bpmn-q/modeler-component/editor/ui/SaveButton.js index a3450209..aa00093e 100644 --- a/components/bpmn-q/modeler-component/editor/ui/SaveButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/SaveButton.js @@ -9,20 +9,24 @@ import { saveModelerAsLocalFile } from "../util/IoUtilities"; * @constructor */ export default function SaveButton(props) { + const { modeler } = props; - const { modeler } = props; - - return ( -
- + return ( +
+ +
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/TransformationButton.js b/components/bpmn-q/modeler-component/editor/ui/TransformationButton.js index 72872ad0..fdfca914 100644 --- a/components/bpmn-q/modeler-component/editor/ui/TransformationButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/TransformationButton.js @@ -1,4 +1,4 @@ -import React, {useState} from "react"; +import React, { useState } from "react"; /** * React button which contains a transformation function to transform the workflow. The button contains a button and a @@ -9,34 +9,40 @@ import React, {useState} from "react"; * @constructor */ export default function TransformationButton(props) { + const { + title, + name, + className, + selectedCallback, // callback for propagating changes of the checkbox + isChecked, // initial value for the checkbox + } = props; - const { - transformWorkflow, // transformation function of this component - title, - name, - className, - selectedCallback, // callback for propagating changes of the checkbox - isChecked, // initial value for the checkbox - } = props; + const [checked, setChecked] = useState(isChecked); - const [checked, setChecked] = useState(isChecked); + // call selectedCallback if the checkbox changes + const handleCheckboxChange = () => { + setChecked(!checked); + selectedCallback(!checked, name); + }; - // call selectedCallback if the checkbox changes - const handleCheckboxChange = () => { - setChecked(!checked); - selectedCallback(!checked, name); - }; - - return ( -
- - - -
- ); -} \ No newline at end of file + return ( +
+ + +
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/TransformationToolbarButton.js b/components/bpmn-q/modeler-component/editor/ui/TransformationToolbarButton.js index 7dd079d5..56d7133c 100644 --- a/components/bpmn-q/modeler-component/editor/ui/TransformationToolbarButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/TransformationToolbarButton.js @@ -1,8 +1,8 @@ -import React, {useRef, useState, useEffect} from 'react'; +import React, { useRef, useState, useEffect } from "react"; import TransformationButton from "./TransformationButton"; -import {getXml, handleTransformedWorkflow} from '../util/IoUtilities'; -import NotificationHandler from './notifications/NotificationHandler'; -import {getModeler} from '../ModelerHandler'; +import { getXml, handleTransformedWorkflow } from "../util/IoUtilities"; +import NotificationHandler from "./notifications/NotificationHandler"; +import { getModeler } from "../ModelerHandler"; /** * Component which groups the TransformationButtons defined by the plugins. Each button defines a transformation function which @@ -16,166 +16,172 @@ import {getModeler} from '../ModelerHandler'; * @constructor */ export default function TransformationToolbarButton(props) { + const { + subButtons, // list of transformation buttons + title, + styleClass, + } = props; - const { - subButtons, // list of transformation buttons - title, - styleClass, - } = props; + // flag defining if the subButtons should be displayed or not + const [isToggleOn, setToggleOn] = useState(false); - // flag defining if the subButtons should be displayed or not - const [isToggleOn, setToggleOn] = useState(false); + const wrapperRef = useRef(null); - const wrapperRef = useRef(null); + // initially activate all transformations + const initialTransformationStates = {}; + subButtons.forEach(function (button) { + initialTransformationStates[button.props.name] = true; + }); - // initially activate all transformations - const initialTransformationStates = {}; - subButtons.forEach(function (button) { - initialTransformationStates[button.props.name] = true; - }); - - /* + /* Saves whether a transformation should be executed, if the state of a transformation is true, this transformation will be executed */ - const [transformationStates, setTransformationStates] = useState(initialTransformationStates); - - // execute the transformation functions of each button - async function startTransformation() { - - // get current workflow of the modeler as xml string - const modeler = getModeler(); - let xml = await getXml(modeler); - let tmp; - - // show notification if at least one transformation function is active - if (Object.entries(transformationStates).some((state) => state[1])) { - NotificationHandler.getInstance().displayNotification({ - type: 'info', - title: 'Workflow Transformation Started!', - content: 'Successfully started transformation process for the current workflow!', - duration: 7000 - }); - } - - try { - // start all active transformations - for (let transformationButton of subButtons) { - - if (transformationStates[transformationButton.props.name]) { - - console.log('Starting Transformation for ' + transformationButton.props.name); - - // execute transformation function of the button - tmp = await transformationButton.props.transformWorkflow(xml); - - if (tmp && tmp.status === 'transformed') { - xml = tmp.xml; - - } else { - - // break process if one transformation failes - const cause = tmp.cause || 'Transformation failed because of an unexpected error.'; - - NotificationHandler.getInstance().displayNotification({ - type: 'warning', - title: `Unable to transform ${transformationButton.props.name} elements in the workflow`, - content: cause, - duration: 10000 - }); - } - } - } - if (xml) { + const [transformationStates, setTransformationStates] = useState( + initialTransformationStates + ); + + // execute the transformation functions of each button + async function startTransformation() { + // get current workflow of the modeler as xml string + const modeler = getModeler(); + let xml = await getXml(modeler); + let tmp; + + // show notification if at least one transformation function is active + if (Object.entries(transformationStates).some((state) => state[1])) { + NotificationHandler.getInstance().displayNotification({ + type: "info", + title: "Workflow Transformation Started!", + content: + "Successfully started transformation process for the current workflow!", + duration: 7000, + }); + } - // handle transformed workflow (open in new tab, save to file storage etc.) - await handleTransformedWorkflow(xml); - } + try { + // start all active transformations + for (let transformationButton of subButtons) { + if (transformationStates[transformationButton.props.name]) { + console.log( + "Starting Transformation for " + transformationButton.props.name + ); + + // execute transformation function of the button + tmp = await transformationButton.props.transformWorkflow(xml); + + if (tmp && tmp.status === "transformed") { + xml = tmp.xml; + } else { + // break process if one transformation fails + const cause = + tmp.cause || + "Transformation failed because of an unexpected error."; - } catch (error) { NotificationHandler.getInstance().displayNotification({ - type: 'warning', - title: 'Error during transformation', - content: 'An unexpected error occurred during transformation. Please check the formatting of your workflow.', - duration: 10000 + type: "warning", + title: `Unable to transform ${transformationButton.props.name} elements in the workflow`, + content: cause, + duration: 10000, }); - console.log(error); + return; + } } + } + // handle transformed workflow (open in new tab, save to file storage etc.) + await handleTransformedWorkflow(xml); + } catch (error) { + NotificationHandler.getInstance().displayNotification({ + type: "warning", + title: "Error during transformation", + content: + "An unexpected error occurred during transformation. Please check the formatting of your workflow.", + duration: 10000, + }); + console.log(error); } - - // callback to activate/ deactivate a transformation - function selectedCallback(isActive, transformationName) { - const newState = transformationStates; - newState[transformationName] = isActive; - setTransformationStates(newState); + } + + // callback to activate/ deactivate a transformation + function selectedCallback(isActive, transformationName) { + const newState = transformationStates; + newState[transformationName] = isActive; + setTransformationStates(newState); + } + + // opens/ closes subButtons by inverting isToggleOn + function handleClick() { + if (!isToggleOn) { + // dispatch event to close other extensible buttons + const newEvent = new CustomEvent("new-extensible-button-opened", { + detail: { + openButtonId: title + styleClass, + }, + }); + return document.dispatchEvent(newEvent); } - // opens/ closes subButtons by inverting isToggleOn - function handleClick() { - - if (!isToggleOn) { - - // dispatch event to close other extensible buttons - const newEvent = new CustomEvent('new-extensible-button-opened', { - detail: { - openButtonId: title + styleClass, - }, - }); - return document.dispatchEvent(newEvent); - } - - setToggleOn(!isToggleOn); - } - - // close displayed TransformationButtons if another ExtensibleButton is opening - const openingListener = event => { - const currentId = title + styleClass; - setToggleOn(currentId === event.detail.openButtonId); + setToggleOn(!isToggleOn); + } + + // close displayed TransformationButtons if another ExtensibleButton is opening + const openingListener = (event) => { + const currentId = title + styleClass; + setToggleOn(currentId === event.detail.openButtonId); + }; + + useEffect(() => { + document.addEventListener("new-extensible-button-opened", openingListener); + return () => { + document.removeEventListener( + "new-extensible-button-opened", + openingListener + ); }; - - useEffect(() => { - document.addEventListener('new-extensible-button-opened', openingListener); - return () => { - document.removeEventListener('new-extensible-button-opened', openingListener); - }; - }, []); - - return ( -
- - - {isToggleOn && -
- { - subButtons.map(function (entry, index) { - return (); - }) - } -
- } + }, []); + + return ( +
+ + + {isToggleOn && ( +
+ {subButtons.map(function (entry, index) { + return ( + + ); + })}
- ); -} \ No newline at end of file + )} +
+ ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/UploadButton.js b/components/bpmn-q/modeler-component/editor/ui/UploadButton.js index fd1b8a45..43bca713 100644 --- a/components/bpmn-q/modeler-component/editor/ui/UploadButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/UploadButton.js @@ -1,5 +1,5 @@ import React from "react"; -import { uploadToGitHub } from '../../extensions/quantme/qrm-manager/git-handler'; +import { uploadToGitHub } from "../../extensions/quantme/qrm-manager/git-handler"; import { getModeler } from "../ModelerHandler"; /** @@ -9,13 +9,15 @@ import { getModeler } from "../ModelerHandler"; * @constructor */ export default function UploadButton() { - - return ( - - ); -} \ No newline at end of file + return ( + + ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js b/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js index aa59adfd..71fc52f2 100644 --- a/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js +++ b/components/bpmn-q/modeler-component/editor/ui/XMLViewerButton.js @@ -10,54 +10,55 @@ import { loadDiagram } from "../util/IoUtilities"; * @constructor */ export default function XMLViewerButton() { + const [enabledXMLView, setEnabledXMLView] = useState(false); + function enableXMLViewer(enabledXMLView) { + let modelerContainer = document.getElementById("modeler-container"); + let editor = document.getElementById("editor"); + let editorWrap = document.getElementById("editor_wrap"); + let panel = document.getElementById("properties"); + let aceEditor = ace.edit(editor); + if (!enabledXMLView) { + modelerContainer.style.height = "93vh"; + editor.style.display = "block"; + editor.style.height = "93vh"; + panel.style.display = "none"; + editorWrap.style.display = "block"; - const [enabledXMLView, setEnabledXMLView] = useState(false); - function enableXMLViewer(enabledXMLView) { - let modelerContainer = document.getElementById('modeler-container'); - let editor = document.getElementById('editor'); - let editorWrap = document.getElementById('editor_wrap'); - let panel = document.getElementById("properties"); - let aceEditor = ace.edit(editor); - if (!enabledXMLView) { - modelerContainer.style.height = '93vh'; - editor.style.display = 'block'; - editor.style.height = '93vh'; - panel.style.display = 'none'; - editorWrap.style.display = 'block'; + // Dynamically set the value of the editor + let xml = getModeler().xml; + if (xml.xml != undefined) { + xml = xml.xml; + } + aceEditor.setValue(xml); + } else { + modelerContainer.style.height = "98vh"; + editor.style.display = "none"; + panel.style.display = "block"; + editorWrap.style.display = "none"; - // Dynamically set the value of the editor - let xml = getModeler().xml; - if (xml.xml != undefined) { - xml = xml.xml; - } - aceEditor.setValue(xml); - } else { - modelerContainer.style.height = '98vh'; - editor.style.display = 'none'; - panel.style.display = 'block'; - editorWrap.style.display = 'none'; - - - aceEditor.getSession().on('change', function () { - update(); - }); - - function update() { - let xml = aceEditor.getSession().getValue(); - loadDiagram(xml, getModeler()); - } - } - - setEnabledXMLView(!enabledXMLView) + aceEditor.getSession().on("change", function () { + update(); + }); + // eslint-disable-next-line no-inner-declarations + function update() { + let xml = aceEditor.getSession().getValue(); + loadDiagram(xml, getModeler()); + } } - return ( - - ); -} \ No newline at end of file + setEnabledXMLView(!enabledXMLView); + } + + return ( + + ); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/modal/EscapeTrap.js b/components/bpmn-q/modeler-component/editor/ui/modal/EscapeTrap.js index 64d61dc6..b2c2490a 100644 --- a/components/bpmn-q/modeler-component/editor/ui/modal/EscapeTrap.js +++ b/components/bpmn-q/modeler-component/editor/ui/modal/EscapeTrap.js @@ -9,31 +9,28 @@ */ export default function EscapeTrap(onEscape) { - - function handleKeyDown(event) { - if (isEscape(event)) { - onEscape(event); - } + function handleKeyDown(event) { + if (isEscape(event)) { + onEscape(event); } + } - function mount() { - document.addEventListener('keydown', handleKeyDown); - } + function mount() { + document.addEventListener("keydown", handleKeyDown); + } - function unmount() { - document.removeEventListener('keydown', handleKeyDown); - } - - return { - mount, - unmount - }; + function unmount() { + document.removeEventListener("keydown", handleKeyDown); + } + return { + mount, + unmount, + }; } - // helpers /////////////// function isEscape(event) { - return event.key === 'Escape'; + return event.key === "Escape"; } diff --git a/components/bpmn-q/modeler-component/editor/ui/modal/FocusTrap.js b/components/bpmn-q/modeler-component/editor/ui/modal/FocusTrap.js index ea140b29..1ae23514 100644 --- a/components/bpmn-q/modeler-component/editor/ui/modal/FocusTrap.js +++ b/components/bpmn-q/modeler-component/editor/ui/modal/FocusTrap.js @@ -9,115 +9,109 @@ */ const focusableElementsSelector = [ - 'a[href]', - 'button:not([disabled])', - 'area[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - 'iframe', - 'object', - 'embed', - '*[tabindex]', - '*[contenteditable]' + "a[href]", + "button:not([disabled])", + "area[href]", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "iframe", + "object", + "embed", + "*[tabindex]", + "*[contenteditable]", ].join(); export default function FocusTrap(getElement) { + let tabbing = false; - let tabbing = false; + function restoreFocus(target) { + const first = getFirstFocusableElement(), + last = getLastFocusableElement(); - function restoreFocus(target) { - - const first = getFirstFocusableElement(), - last = getLastFocusableElement(); - - // do nothing if there is no focusable element - if (!first) { - return; - } - - if (target !== first) { - first.focus(); - } else { - last.focus(); - } - } - - function handleBlur(event) { - - // do nothing if focus stays inside the modal - if (getElement().contains(event.relatedTarget)) { - return; - } - - if (!tabbing) { - return; - } - - restoreFocus(event.target); + // do nothing if there is no focusable element + if (!first) { + return; } - function handleKeyDown(event) { - if (isTab(event)) { - tabbing = true; - } + if (target !== first) { + first.focus(); + } else { + last.focus(); } + } - function handleKeyUp(event) { - tabbing = false; + function handleBlur(event) { + // do nothing if focus stays inside the modal + if (getElement().contains(event.relatedTarget)) { + return; } - function getFirstFocusableElement() { - return getFocusableElements()[0]; + if (!tabbing) { + return; } - function getLastFocusableElement() { - const elements = getFocusableElements(); + restoreFocus(event.target); + } - return elements[elements.length - 1]; + function handleKeyDown(event) { + if (isTab(event)) { + tabbing = true; } + } - function getFocusableElements() { - return getElement().querySelectorAll(focusableElementsSelector); - } + function handleKeyUp() { + tabbing = false; + } - function focus() { + function getFirstFocusableElement() { + return getFocusableElements()[0]; + } - // focus the first focusable element if currently - // focussed element is outside the modal - if (getElement().contains(document.activeElement)) { - return; - } + function getLastFocusableElement() { + const elements = getFocusableElements(); - const focusable = getFirstFocusableElement(); + return elements[elements.length - 1]; + } - focusable && focusable.focus(); + function getFocusableElements() { + return getElement().querySelectorAll(focusableElementsSelector); + } + + function focus() { + // focus the first focusable element if currently + // focussed element is outside the modal + if (getElement().contains(document.activeElement)) { + return; } - function mount() { - focus(); + const focusable = getFirstFocusableElement(); - document.addEventListener('blur', handleBlur, true); - document.addEventListener('keydown', handleKeyDown); - document.addEventListener('keyup', handleKeyUp); - } + focusable && focusable.focus(); + } - function unmount() { - document.removeEventListener('blur', handleBlur, true); - document.removeEventListener('keydown', handleKeyDown); - document.removeEventListener('keyup', handleKeyUp); - } + function mount() { + focus(); - return { - mount, - unmount - }; + document.addEventListener("blur", handleBlur, true); + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("keyup", handleKeyUp); + } -} + function unmount() { + document.removeEventListener("blur", handleBlur, true); + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("keyup", handleKeyUp); + } + return { + mount, + unmount, + }; +} // helpers /////////////// function isTab(event) { - return event.key === 'Tab'; + return event.key === "Tab"; } diff --git a/components/bpmn-q/modeler-component/editor/ui/modal/KeyboardInteractionTrap.js b/components/bpmn-q/modeler-component/editor/ui/modal/KeyboardInteractionTrap.js index ae12c0b0..c09af83d 100644 --- a/components/bpmn-q/modeler-component/editor/ui/modal/KeyboardInteractionTrap.js +++ b/components/bpmn-q/modeler-component/editor/ui/modal/KeyboardInteractionTrap.js @@ -8,10 +8,9 @@ * except in compliance with the MIT License. */ -import React, {PureComponent, createContext} from 'react'; +import React, { PureComponent, createContext } from "react"; -export const KeyboardInteractionTrapContext = createContext(() => { -}); +export const KeyboardInteractionTrapContext = createContext(() => {}); /** * A wrapper around a react component that ensures that @@ -26,72 +25,70 @@ export const KeyboardInteractionTrapContext = createContext(() => { * is a modal that user and keyboard focus. */ export default function KeyboardInteractionTrap(props) { - return ( - - {triggerAction => ( - - {props.children || null} - - )} - - ); + return ( + + {(triggerAction) => ( + + {props.children || null} + + )} + + ); } class KeyboardInteractionTrapComponent extends PureComponent { + handleFocus = (event) => { + this.updateMenu(event.target); + }; - handleFocus = (event) => { - this.updateMenu(event.target); - }; + updateMenu(element) { + const enabled = ["INPUT", "TEXTAREA"].includes(element.tagName); - updateMenu(element) { + const editMenu = [ + [ + { + role: "undo", + enabled, + }, + { + role: "redo", + enabled, + }, + ], + [ + { + role: "copy", + enabled, + }, + { + role: "cut", + enabled, + }, + { + role: "paste", + enabled, + }, + { + role: "selectAll", + enabled, + }, + ], + ]; - const enabled = ['INPUT', 'TEXTAREA'].includes(element.tagName); + this.props.triggerAction("update-menu", { editMenu }); + } - const editMenu = [ - [ - { - role: 'undo', - enabled - }, - { - role: 'redo', - enabled - }, - ], - [ - { - role: 'copy', - enabled - }, - { - role: 'cut', - enabled - }, - { - role: 'paste', - enabled - }, - { - role: 'selectAll', - enabled - } - ] - ]; + componentDidMount() { + window.addEventListener("focus", this.handleFocus); - this.props.triggerAction('update-menu', {editMenu}); - } + this.updateMenu(document.activeElement); + } - componentDidMount() { - window.addEventListener('focus', this.handleFocus); + componentWillUnmount() { + window.removeEventListener("focus", this.handleFocus); + } - this.updateMenu(document.activeElement); - } - - componentWillUnmount() { - window.removeEventListener('focus', this.handleFocus); - } - - render() { - return this.props.children; - } + render() { + return this.props.children; + } } diff --git a/components/bpmn-q/modeler-component/editor/ui/modal/Modal.js b/components/bpmn-q/modeler-component/editor/ui/modal/Modal.js index fdb41868..d686695e 100644 --- a/components/bpmn-q/modeler-component/editor/ui/modal/Modal.js +++ b/components/bpmn-q/modeler-component/editor/ui/modal/Modal.js @@ -7,72 +7,70 @@ * Camunda licenses this file to you under the MIT; you may not use this file * except in compliance with the MIT License. */ -import React, {PureComponent} from 'react'; -import ReactDOM from 'react-dom'; +import React, { PureComponent } from "react"; +import ReactDOM from "react-dom"; -import classNames from 'classnames'; +import classNames from "classnames"; -import FocusTrap from './FocusTrap'; -import EscapeTrap from './EscapeTrap'; -import KeyboardInteractionTrap from './KeyboardInteractionTrap'; +import FocusTrap from "./FocusTrap"; +import EscapeTrap from "./EscapeTrap"; +import KeyboardInteractionTrap from "./KeyboardInteractionTrap"; /** * React component to display a modal. */ export default class Modal extends PureComponent { + constructor(props) { + super(props); - constructor(props) { - super(props); + this.modalRef = React.createRef(); - this.modalRef = React.createRef(); + this.focusTrap = FocusTrap(() => { + return this.modalRef.current; + }); - this.focusTrap = FocusTrap(() => { - return this.modalRef.current; - }); + this.escapeTrap = EscapeTrap(() => { + this.close(); + }); + } - this.escapeTrap = EscapeTrap(() => { - this.close(); - }); - } - - close = () => { - if (this.props.onClose) { - return this.props.onClose(); - } - }; - - componentDidMount() { - this.focusTrap.mount(); - this.escapeTrap.mount(); - } - - componentWillUnmount() { - this.focusTrap.unmount(); - this.escapeTrap.unmount(); - } - - render() { - - const { - className, - children, - onClose - } = this.props; - - return ReactDOM.createPortal( - -
-
-
- {children} - {onClose && ()} -
-
-
-
, - document.body - ); + close = () => { + if (this.props.onClose) { + return this.props.onClose(); } + }; + + componentDidMount() { + this.focusTrap.mount(); + this.escapeTrap.mount(); + } + + componentWillUnmount() { + this.focusTrap.unmount(); + this.escapeTrap.unmount(); + } + + render() { + const { className, children, onClose } = this.props; + + return ReactDOM.createPortal( + +
+
+
+ {children} + {onClose && } +
+
+
+
, + document.body + ); + } } Modal.Body = Body; @@ -83,65 +81,55 @@ Modal.Close = Close; Modal.Footer = Footer; - function Title(props) { - const { - children, - className, - ...rest - } = props; - - return ( -
-

- {children} -

-
- ); + const { children, className, ...rest } = props; + + return ( +
+

{children}

+
+ ); } function Close(props) { - const { - onClick - } = props; - - return ( - - ); + const { onClick } = props; + + return ( + + ); } function Body(props) { - const { - children, - className, - ...rest - } = props; - - return ( -
- {children} -
- ); + const { children, className, ...rest } = props; + + return ( +
+ {children} +
+ ); } function Footer(props) { - const { - children, - className, - ...rest - } = props; - - return ( -
- {props.children} -
- ); + const { className, ...rest } = props; + + return ( +
+ {props.children} +
+ ); } diff --git a/components/bpmn-q/modeler-component/editor/ui/modal/index.js b/components/bpmn-q/modeler-component/editor/ui/modal/index.js index 66958478..e4020b9e 100644 --- a/components/bpmn-q/modeler-component/editor/ui/modal/index.js +++ b/components/bpmn-q/modeler-component/editor/ui/modal/index.js @@ -8,4 +8,4 @@ * except in compliance with the MIT License. */ -export {default as Modal} from './Modal'; +export { default as Modal } from "./Modal"; diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.css b/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.css index fdb63990..33d47382 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.css +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.css @@ -1,39 +1,39 @@ .qwm-Notification { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 12px; - box-sizing: border-box; - width: 300px; - background: var(--color-ffffff); - box-shadow: 0 1px 4px var(--color-000000-opacity-10); - border-radius: 2px; - padding: 20px; - margin-bottom: 20px; + font-size: 12px; + box-sizing: border-box; + width: 300px; + background: var(--color-ffffff); + box-shadow: 0 1px 4px var(--color-000000-opacity-10); + border-radius: 2px; + padding: 20px; + margin-bottom: 20px; } .qwm-Notification h2 { - margin: 0; - font-weight: normal; + margin: 0; + font-weight: normal; } .qwm-Notification .qwm-content { - margin-top: 10px; + margin-top: 10px; } .qwm-Notification .qwm-close { - vertical-align: middle; - padding: 0 2px; - margin-left: 3px; - font-size: 18px; - line-height: 9px; - width: 13px; - float: right; + vertical-align: middle; + padding: 0 2px; + margin-left: 3px; + font-size: 18px; + line-height: 9px; + width: 13px; + float: right; } .qwm-Notification .qwm-close:before { - content: '×'; + content: "×"; } .qwm-Notification .qwm-close:hover { - color: var(--blue-darken-48); -} \ No newline at end of file + color: var(--blue-darken-48); +} diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.js b/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.js index 3cafffec..df088c03 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/Notification.js @@ -8,9 +8,9 @@ * except in compliance with the MIT License. */ -import React, {PureComponent} from 'react'; +import React, { PureComponent } from "react"; -export const NOTIFICATION_TYPES = ['info', 'success', 'error', 'warning']; +export const NOTIFICATION_TYPES = ["info", "success", "error", "warning"]; /** * React component to display notifications @@ -18,65 +18,61 @@ export const NOTIFICATION_TYPES = ['info', 'success', 'error', 'warning']; * @type {string[]} */ export default class Notification extends PureComponent { - static getDerivedStateFromError() { - return {error: true}; - } + static getDerivedStateFromError() { + return { error: true }; + } - state = { - error: false - }; + state = { + error: false, + }; - componentDidMount() { - const {duration} = this.props; + componentDidMount() { + const { duration } = this.props; - if (duration) { - this.setupTimeout(duration); - } + if (duration) { + this.setupTimeout(duration); } + } - componentDidUpdate(previousProps) { - const currentDuration = this.props.duration; - - const {duration: previousDuration} = previousProps; - - if (currentDuration !== previousDuration) { - this.resetTimeout(); + componentDidUpdate(previousProps) { + const currentDuration = this.props.duration; - currentDuration && this.setupTimeout(currentDuration); - } - } - - componentWillUnmount() { - this.resetTimeout(); - } + const { duration: previousDuration } = previousProps; - setupTimeout(duration) { - this.timeout = setTimeout(() => { - this.props.close(); - }, duration); - } - - resetTimeout() { - this.timeout && clearTimeout(this.timeout); - } - - componentDidCatch() { - this.props.close(); - } + if (currentDuration !== previousDuration) { + this.resetTimeout(); - render() { - const { - close, - content, - title, - } = this.props; - - return this.state.error ? null :
- -

- {title} -

- {content &&
{content}
} -
; + currentDuration && this.setupTimeout(currentDuration); } + } + + componentWillUnmount() { + this.resetTimeout(); + } + + setupTimeout(duration) { + this.timeout = setTimeout(() => { + this.props.close(); + }, duration); + } + + resetTimeout() { + this.timeout && clearTimeout(this.timeout); + } + + componentDidCatch() { + this.props.close(); + } + + render() { + const { close, content, title } = this.props; + + return this.state.error ? null : ( +
+ +

{title}

+ {content &&
{content}
} +
+ ); + } } diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js b/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js index b65579b7..ba1ce2c4 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/NotificationHandler.js @@ -1,7 +1,7 @@ import React from "react"; import Notifications from "./Notifications"; -export const NOTIFICATION_TYPES = ['info', 'success', 'error', 'warning']; +export const NOTIFICATION_TYPES = ["info", "success", "error", "warning"]; /** * Handler to manage notifications displayed to the user. Use getInstance() to get the current instance of the handler. @@ -9,62 +9,66 @@ export const NOTIFICATION_TYPES = ['info', 'success', 'error', 'warning']; * Implements the Singleton pattern. */ export default class NotificationHandler { + static instance = undefined; - static instance = undefined; - - static getInstance() { - if (this.instance) { - return this.instance; - } else { - this.instance = new NotificationHandler([]); - return this.instance; - } + static getInstance() { + if (this.instance) { + return this.instance; + } else { + this.instance = new NotificationHandler([]); + return this.instance; } + } - constructor(notifications) { - this.notifications = notifications; - this.currentNotificationId = -1; - this.notificationRef = React.createRef(); - } + constructor(notifications) { + this.notifications = notifications; + this.currentNotificationId = -1; + this.notificationRef = React.createRef(); + } - /** - * Creates a new Notifications React Component with a fixed ref to access the methods of the component. - * - * @param notifications The initial set of components to display wright after creation. - * @param notificationsContainer DOM element the notifications are rendered into. - * @returns the created Notifications React Component - */ - createNotificationsComponent(notifications, notificationsContainer) { - if (notifications) { - this.notifications = notifications; - } - return ; + /** + * Creates a new Notifications React Component with a fixed ref to access the methods of the component. + * + * @param notifications The initial set of components to display wright after creation. + * @param notificationsContainer DOM element the notifications are rendered into. + * @returns the created Notifications React Component + */ + createNotificationsComponent(notifications, notificationsContainer) { + if (notifications) { + this.notifications = notifications; } + return ( + + ); + } - /** - * Creates and displays a new Notification with the given properties. Calls effectively the respective method of the - * Notification Component. - * - * @param type The NOTIFICATION_TYPES of the notification. - * @param title The title of the notification. - * @param content The text displayed by the notification. - * @param duration The duration in milliseconds. - * @returns {{update: update, close: close}} - */ - displayNotification({type = 'info', title, content, duration = 4000}) { - return this.notificationRef.current.displayNotification({ - type: type, - title: title, - content: content, - duration: duration - }); - } + /** + * Creates and displays a new Notification with the given properties. Calls effectively the respective method of the + * Notification Component. + * + * @param type The NOTIFICATION_TYPES of the notification. + * @param title The title of the notification. + * @param content The text displayed by the notification. + * @param duration The duration in milliseconds. + * @returns {{update: update, close: close}} + */ + displayNotification({ type = "info", title, content, duration = 4000 }) { + return this.notificationRef.current.displayNotification({ + type: type, + title: title, + content: content, + duration: duration, + }); + } - /** - * Close all open notifications. - */ - closeNotifications() { - this.notificationRef.current.closeNotifications(); - } -} \ No newline at end of file + /** + * Close all open notifications. + */ + closeNotifications() { + this.notificationRef.current.closeNotifications(); + } +} diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.css b/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.css index 2baffc9e..3ed2dddd 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.css +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.css @@ -1,13 +1,13 @@ .qwm-Notifications { - display: flex; - flex-direction: column-reverse; - z-index: 500; - position: absolute; - top: 100px; - right: 0; - left: 0; - margin-left: auto; - margin-right: auto; - width: 300px; - height: auto; -} \ No newline at end of file + display: flex; + flex-direction: column-reverse; + z-index: 500; + position: absolute; + top: 100px; + right: 0; + left: 0; + margin-left: auto; + margin-right: auto; + width: 300px; + height: auto; +} diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js b/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js index 19b67193..b4703818 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/Notifications.js @@ -8,113 +8,115 @@ * except in compliance with the MIT License. */ -import React, {PureComponent} from 'react'; -import {createPortal} from 'react-dom'; -import Notification from './Notification'; -import {NOTIFICATION_TYPES} from "./NotificationHandler"; +import React, { PureComponent } from "react"; +import { createPortal } from "react-dom"; +import Notification from "./Notification"; +import { NOTIFICATION_TYPES } from "./NotificationHandler"; /** * React component to manage Notification components */ export default class Notifications extends PureComponent { - constructor(props) { - super(props); - - this.container = props.container; - this.state = { - notifications: props.notifications || [] - }; - this.currentNotificationId = -1; + constructor(props) { + super(props); + + this.container = props.container; + this.state = { + notifications: props.notifications || [], + }; + this.currentNotificationId = -1; + } + + /** + * Display new Notification of the given type, title and content. Define the duration the Notification should + * be displayed by duration. + * + * @param type The type of the Notification. + * @param title The title of the Notification. + * @param content The content of the Notification. + * @param duration The duration of the Notification. + * @returns {{update: update, close: close}} + */ + displayNotification({ type = "info", title, content, duration = 4000 }) { + const notifications = this.state.notifications; + + if (!NOTIFICATION_TYPES.includes(type)) { + throw new Error("Unknown notification type"); } - /** - * Display new Notification of the given type, title and content. Define the duration the Notification should - * be displayed by duration. - * - * @param type The type of the Notification. - * @param title The title of the Notification. - * @param content The content of the Notification. - * @param duration The duration of the Notification. - * @returns {{update: update, close: close}} - */ - displayNotification({type = 'info', title, content, duration = 4000}) { - const notifications = this.state.notifications; - - if (!NOTIFICATION_TYPES.includes(type)) { - throw new Error('Unknown notification type'); - } - - const id = this.currentNotificationId++; - - const close = () => { - console.log('close'); - this._closeNotification(id); - }; - - const update = newProps => { - this._updateNotification(id, newProps); - }; - - const notification = { - content, - duration, - id, - close, - title, - type - }; - - this.setState({ - notifications: [ - ...notifications, - notification - ] - }); - - return { - close, - update - }; - } - - /** - * Close all displayed Notifications. - */ - closeNotifications() { - this.setState({notifications: []}); - } - - _updateNotification(id, options) { - const notifications = this.state.notifications.map(notification => { - const {id: currentId} = notification; - - return currentId !== id ? notification : {...notification, ...options}; - }); - - this.setState({notifications: notifications}); - } - - /** - * Close the Notification with the given ID. - * - * @param id The ID of the Notification to close. - * @private - */ - _closeNotification(id) { - const notifications = this.state.notifications.filter(({id: currentId}) => currentId !== id); - this.setState({notifications: notifications}); - } - - render() { - let { - notifications - } = this.state; - notifications = notifications || []; - const notificationComponents = notifications.map(({id, ...props}) => { - return ; - }).reverse(); - - // className={ css.Notifications } - return createPortal(
{notificationComponents}
, this.container); - } -} \ No newline at end of file + const id = this.currentNotificationId++; + + const close = () => { + console.log("close"); + this._closeNotification(id); + }; + + const update = (newProps) => { + this._updateNotification(id, newProps); + }; + + const notification = { + content, + duration, + id, + close, + title, + type, + }; + + this.setState({ + notifications: [...notifications, notification], + }); + + return { + close, + update, + }; + } + + /** + * Close all displayed Notifications. + */ + closeNotifications() { + this.setState({ notifications: [] }); + } + + _updateNotification(id, options) { + const notifications = this.state.notifications.map((notification) => { + const { id: currentId } = notification; + + return currentId !== id ? notification : { ...notification, ...options }; + }); + + this.setState({ notifications: notifications }); + } + + /** + * Close the Notification with the given ID. + * + * @param id The ID of the Notification to close. + * @private + */ + _closeNotification(id) { + const notifications = this.state.notifications.filter( + ({ id: currentId }) => currentId !== id + ); + this.setState({ notifications: notifications }); + } + + render() { + let { notifications } = this.state; + notifications = notifications || []; + const notificationComponents = notifications + .map(({ id, ...props }) => { + return ; + }) + .reverse(); + + // className={ css.Notifications } + return createPortal( +
{notificationComponents}
, + this.container + ); + } +} diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationSpec.js b/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationSpec.js index 647f922d..50d66299 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationSpec.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationSpec.js @@ -10,44 +10,36 @@ /* global sinon */ -import React from 'react'; +import React from "react"; -import { - mount, - shallow -} from 'enzyme'; +import { mount, shallow } from "enzyme"; -import Notification from '../Notification'; -import {describe, it, after, before} from "mocha"; +import Notification from "../Notification"; +import { describe, it, after, before } from "mocha"; -const { expect } = require('chai') +const { expect } = require("chai"); -describe('', function() { - - it('should render', function() { +describe("", function () { + it("should render", function () { shallow(); }); - - describe('duration', function() { - + describe("duration", function () { let clock; - before(function() { + before(function () { clock = sinon.useFakeTimers(); }); - after(function() { + after(function () { clock.restore(); }); - - it('should close automatically when is set', function() { - + it("should close automatically when is set", function () { // given const closeSpy = sinon.spy(); - shallow(); + shallow(); // when clock.tick(1000); @@ -56,13 +48,13 @@ describe('', function() { expect(closeSpy).to.have.been.calledOnce; }); - - it('should handle changes', function() { - + it("should handle changes", function () { // given const closeSpy = sinon.spy(); - const notification = shallow(); + const notification = shallow( + + ); // when notification.setProps({ duration: 2000 }); @@ -79,13 +71,11 @@ describe('', function() { expect(closeSpy).to.have.been.calledOnce; }); - - it('should NOT close automatically when is NOT set', function() { - + it("should NOT close automatically when is NOT set", function () { // given const closeSpy = sinon.spy(); - shallow(); + shallow(); // when clock.tick(10000); @@ -93,27 +83,22 @@ describe('', function() { // then expect(closeSpy).to.have.not.been.called; }); - }); - - describe('error boundary', function() { - + describe("error boundary", function () { let wrapper; - afterEach(function() { + afterEach(function () { wrapper && wrapper.unmount(); }); - - it('should close notification if it throws', function() { - + it("should close notification if it throws", function () { // given - const Content = () => 'content'; + const Content = () => "content"; const closeSpy = sinon.spy(); - wrapper = mount( } />); + wrapper = mount(} />); // when wrapper.find(Content).simulateError(new Error()); @@ -121,7 +106,5 @@ describe('', function() { // then expect(closeSpy).to.have.been.calledOnce; }); - }); - }); diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationsSpec.js b/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationsSpec.js index fb25bb12..8ba39f03 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationsSpec.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/__tests__/NotificationsSpec.js @@ -8,35 +8,30 @@ * except in compliance with the MIT License. */ -import React from 'react'; +import React from "react"; -import { shallow } from 'enzyme'; +import { shallow } from "enzyme"; -import Notifications from '..'; -import Notification from '../Notification'; +import Notifications from ".."; +import Notification from "../Notification"; +import { expect } from "chai"; - -describe('', function() { - - it('should render', function() { - shallow(); +describe("", function () { + it("should render", function () { + shallow(); }); - - it('should display notification', function() { - + it("should display notification", function () { // given const notification = createNotification(); - const wrapper = shallow(); + const wrapper = shallow(); // then expect(wrapper.find(Notification)).to.have.lengthOf(1); }); - }); - // helpers ////////// function createNotification({ @@ -44,8 +39,8 @@ function createNotification({ content, duration = 0, id = 0, - title = 'title', - type = 'info' + title = "title", + type = "info", } = {}) { return { close, @@ -53,6 +48,6 @@ function createNotification({ duration, id, title, - type + type, }; } diff --git a/components/bpmn-q/modeler-component/editor/ui/notifications/index.js b/components/bpmn-q/modeler-component/editor/ui/notifications/index.js index a9f20564..b0ac5396 100644 --- a/components/bpmn-q/modeler-component/editor/ui/notifications/index.js +++ b/components/bpmn-q/modeler-component/editor/ui/notifications/index.js @@ -8,7 +8,7 @@ * except in compliance with the MIT License. */ -import {default as Notifications} from './Notifications'; +import { default as Notifications } from "./Notifications"; export default Notifications; -export {NOTIFICATION_TYPES} from './Notification'; \ No newline at end of file +export { NOTIFICATION_TYPES } from "./Notification"; diff --git a/components/bpmn-q/modeler-component/editor/util/IoUtilities.js b/components/bpmn-q/modeler-component/editor/util/IoUtilities.js index 646d36a9..fa6ca2d6 100644 --- a/components/bpmn-q/modeler-component/editor/util/IoUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/IoUtilities.js @@ -1,26 +1,32 @@ -import { autoSaveFile, saveFileFormats, transformedWorkflowHandlers, workflowEventTypes } from "../EditorConstants"; +import { + autoSaveFile, + saveFileFormats, + transformedWorkflowHandlers, + workflowEventTypes, +} from "../EditorConstants"; import { getModeler } from "../ModelerHandler"; import { dispatchWorkflowEvent } from "../events/EditorEventHandler"; import fetch from "node-fetch"; -const editorConfig = require('../config/EditorConfigManager'); +const editorConfig = require("../config/EditorConfigManager"); -let FormData = require('form-data'); +let FormData = require("form-data"); // workflow with a start event to use as template for new workflows -const NEW_DIAGRAM_XML = '\n' + - '\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ''; +const NEW_DIAGRAM_XML = + '\n' + + '\n' + + ' \n' + + ' \n' + + " \n" + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + " \n" + + " \n" + + " \n" + + ""; /** * Saves a given bpmn diagram as a bpmn file to the locale storage of the user. @@ -29,15 +35,22 @@ const NEW_DIAGRAM_XML = '\n' + * @param fileName The name of the file. * @returns {Promise} */ -export async function saveXmlAsLocalFile(xml, fileName = editorConfig.getFileName()) { - const bpmnFile = await new File([xml], fileName, { type: 'text/xml' }); - - const link = document.createElement('a'); - link.download = fileName + '.bpmn'; - link.href = URL.createObjectURL(bpmnFile); - link.click(); - - dispatchWorkflowEvent(workflowEventTypes.SAVED, xml, editorConfig.getFileName()); +export async function saveXmlAsLocalFile( + xml, + fileName = editorConfig.getFileName() +) { + const bpmnFile = await new File([xml], fileName, { type: "text/xml" }); + + const link = document.createElement("a"); + link.download = fileName + ".bpmn"; + link.href = URL.createObjectURL(bpmnFile); + link.click(); + + dispatchWorkflowEvent( + workflowEventTypes.SAVED, + xml, + editorConfig.getFileName() + ); } /** @@ -47,19 +60,32 @@ export async function saveXmlAsLocalFile(xml, fileName = editorConfig.getFileNam * @param fileName The name of the file. * @returns {Promise} */ -export async function saveModelerAsLocalFile(modeler, fileName = editorConfig.getFileName(), fileFormat = editorConfig.getFileFormat(), openWindow = true) { - const xml = await getXml(modeler); - if (fileFormat === saveFileFormats.BPMN || fileFormat === saveFileFormats.ALL) { - if (openWindow) { - await openFileDialog(xml, fileName, saveFileFormats.BPMN); - } else { - await saveXmlAsLocalFile(xml, fileName); - } - } - - if (fileFormat === saveFileFormats.ALL || fileFormat === saveFileFormats.SVG || fileFormat === saveFileFormats.PNG) { - await saveWorkflowAsSVG(modeler, fileName, fileFormat); +export async function saveModelerAsLocalFile( + modeler, + fileName = editorConfig.getFileName(), + fileFormat = editorConfig.getFileFormat(), + openWindow = true +) { + const xml = await getXml(modeler); + if ( + fileFormat === saveFileFormats.BPMN || + fileFormat === saveFileFormats.ALL + ) { + if (openWindow) { + await openFileDialog(modeler, xml, fileName, saveFileFormats.BPMN); + } else { + modeler.oldXml = xml; + await saveXmlAsLocalFile(xml, fileName); } + } + + if ( + fileFormat === saveFileFormats.ALL || + fileFormat === saveFileFormats.SVG || + fileFormat === saveFileFormats.PNG + ) { + await saveWorkflowAsSVG(modeler, fileName, fileFormat); + } } /** @@ -69,8 +95,8 @@ export async function saveModelerAsLocalFile(modeler, fileName = editorConfig.ge * @returns {Promise<*>} The xml diagram. */ export async function getXml(modeler) { - const { xml } = await modeler.saveXML({ format: true }); - return xml; + const { xml } = await modeler.saveXML({ format: true }); + return xml; } /** @@ -82,22 +108,26 @@ export async function getXml(modeler) { * @returns {Promise} Undefined, if an error occurred during import. */ export async function loadDiagram(xml, modeler, dispatchEvent = true) { - if (xml !== '') { - try { - const result = await modeler.importXML(xml); - modeler.xml = xml; - - if (dispatchEvent) { - dispatchWorkflowEvent(workflowEventTypes.LOADED, xml, editorConfig.getFileName()); - } - - return result; - } catch (err) { - console.error(err); - - return { error: err }; - } + if (xml !== "") { + try { + const result = await modeler.importXML(xml); + modeler.xml = xml; + + if (dispatchEvent) { + dispatchWorkflowEvent( + workflowEventTypes.LOADED, + xml, + editorConfig.getFileName() + ); + } + + return result; + } catch (err) { + console.error(err); + + return { error: err }; } + } } /** @@ -106,7 +136,7 @@ export async function loadDiagram(xml, modeler, dispatchEvent = true) { * @param modeler the given modeler to open the new bpmn diagram in. */ export function createNewDiagram(modeler) { - loadDiagram(NEW_DIAGRAM_XML, modeler).then(); + loadDiagram(NEW_DIAGRAM_XML, modeler).then(); } /** @@ -117,70 +147,103 @@ export function createNewDiagram(modeler) { * @param viewMap a list of views to deploy with the workflow, i.e., the name of the view and the corresponding xml * @return {Promise<{status: string}>} a promise with the deployment status as well as the endpoint of the deployed workflow if successful */ -export async function deployWorkflowToCamunda(workflowName, workflowXml, viewMap) { - console.log('Deploying workflow to Camunda Engine at endpoint: %s', editorConfig.getCamundaEndpoint()); - - // add required form data fields - const form = new FormData(); - form.append('deployment-name', workflowName); - form.append('deployment-source', 'QuantME Modeler'); - form.append('deploy-changed-only', 'false'); - - // add bpmn file ending if not present - let fileName = workflowName; - if (!fileName.endsWith('.bpmn')) { - fileName = fileName + '.bpmn'; - } - - // add diagram to the body - const bpmnFile = new File([workflowXml], fileName, { type: 'text/xml' }); - form.append('data', bpmnFile); - - // upload all provided views - for (const [key, value] of Object.entries(viewMap)) { - console.info('Adding view with name: ', key); - - // add view xml to the body - form.append(key, value, { - filename: fileName.replace('.bpmn', key + '.xml'), - contentType: 'text/xml' - }); - } - - // make the request and wait for the response of the deployment endpoint - try { - const response = await fetch(editorConfig.getCamundaEndpoint() + '/deployment/create', { - method: 'POST', - body: form, - }); - - if (response.ok) { - - // retrieve deployment results from response - const result = await response.json(); - console.info('Deployment provides result: ', result); - console.info('Deployment successful with deployment id: %s', result['id']); - - // abort if there is not exactly one deployed process definition - if (Object.values(result['deployedProcessDefinitions'] || {}).length !== 1) { - console.error('Invalid size of deployed process definitions list: ' + Object.values(result['deployedProcessDefinitions'] || {}).length); - return { status: 'failed' }; - } - - dispatchWorkflowEvent(workflowEventTypes.DEPLOYED, workflowXml, workflowName); - - return { - status: 'deployed', - deployedProcessDefinition: Object.values(result['deployedProcessDefinitions'] || {})[0] - }; - } else { - console.error('Deployment of workflow returned invalid status code: %s', response.status); - return { status: 'failed', message: 'Deployment of workflow returned invalid response: ' + response }; - } - } catch (error) { - console.error('Error while executing post to deploy workflow: ' + error); - return { status: 'failed', message: 'Error while executing post to deploy workflow: ' + error }; +export async function deployWorkflowToCamunda( + workflowName, + workflowXml, + viewMap +) { + console.log( + "Deploying workflow to Camunda Engine at endpoint: %s", + editorConfig.getCamundaEndpoint() + ); + + // add required form data fields + const form = new FormData(); + form.append("deployment-name", workflowName); + form.append("deployment-source", "QuantME Modeler"); + form.append("deploy-changed-only", "false"); + + // add bpmn file ending if not present + let fileName = workflowName; + if (!fileName.endsWith(".bpmn")) { + fileName = fileName + ".bpmn"; + } + + // add diagram to the body + const bpmnFile = new File([workflowXml], fileName, { type: "text/xml" }); + form.append("data", bpmnFile); + + // upload all provided views + for (const [key, value] of Object.entries(viewMap)) { + console.info("Adding view with name: ", key); + + // add view xml to the body + form.append(key, value, { + filename: fileName.replace(".bpmn", key + ".xml"), + contentType: "text/xml", + }); + } + + // make the request and wait for the response of the deployment endpoint + try { + const response = await fetch( + editorConfig.getCamundaEndpoint() + "/deployment/create", + { + method: "POST", + body: form, + } + ); + + if (response.ok) { + // retrieve deployment results from response + const result = await response.json(); + console.info("Deployment provides result: ", result); + console.info( + "Deployment successful with deployment id: %s", + result["id"] + ); + + // abort if there is not exactly one deployed process definition + if ( + Object.values(result["deployedProcessDefinitions"] || {}).length !== 1 + ) { + console.error( + "Invalid size of deployed process definitions list: " + + Object.values(result["deployedProcessDefinitions"] || {}).length + ); + return { status: "failed" }; + } + + dispatchWorkflowEvent( + workflowEventTypes.DEPLOYED, + workflowXml, + workflowName + ); + + return { + status: "deployed", + deployedProcessDefinition: Object.values( + result["deployedProcessDefinitions"] || {} + )[0], + }; + } else { + console.error( + "Deployment of workflow returned invalid status code: %s", + response.status + ); + return { + status: "failed", + message: + "Deployment of workflow returned invalid response: " + response, + }; } + } catch (error) { + console.error("Error while executing post to deploy workflow: " + error); + return { + status: "failed", + message: "Error while executing post to deploy workflow: " + error, + }; + } } /** @@ -191,28 +254,33 @@ export async function deployWorkflowToCamunda(workflowName, workflowXml, viewMap * @returns {Promise} */ export async function handleTransformedWorkflow(workflowXml) { - const fileName = editorConfig.getFileName().split('.')[0] + '_transformed.bpmn'; - - // dispatch workflow transformed event - const eventNotCaught = dispatchWorkflowEvent(workflowEventTypes.TRANSFORMED, workflowXml, fileName); - - console.log(`Transformed Workflow Event caught? - ${eventNotCaught}`); - - // execute respective handle function if event was not already solved - if (eventNotCaught) { - const handlerId = editorConfig.getTransformedWorkflowHandler(); - - switch (handlerId) { - case transformedWorkflowHandlers.NEW_TAB: // open workflow in new browser tab - openInNewTab(workflowXml, fileName); - break; - case transformedWorkflowHandlers.SAVE_AS_FILE: // save workflow to local file system - await saveXmlAsLocalFile(workflowXml, fileName); - break; - default: - console.log(`Invalid transformed workflow handler ID ${handlerId}`); - } + const fileName = + editorConfig.getFileName().split(".")[0] + "_transformed.bpmn"; + + // dispatch workflow transformed event + const eventNotCaught = dispatchWorkflowEvent( + workflowEventTypes.TRANSFORMED, + workflowXml, + fileName + ); + + console.log(`Transformed Workflow Event caught? - ${eventNotCaught}`); + + // execute respective handle function if event was not already solved + if (eventNotCaught) { + const handlerId = editorConfig.getTransformedWorkflowHandler(); + + switch (handlerId) { + case transformedWorkflowHandlers.NEW_TAB: // open workflow in new browser tab + openInNewTab(workflowXml, fileName); + break; + case transformedWorkflowHandlers.SAVE_AS_FILE: // save workflow to local file system + await saveXmlAsLocalFile(workflowXml, fileName); + break; + default: + console.log(`Invalid transformed workflow handler ID ${handlerId}`); } + } } /** @@ -222,125 +290,149 @@ export async function handleTransformedWorkflow(workflowXml) { * @param fileName The name of the workflow. */ export function openInNewTab(workflowXml, fileName) { - - const newWindow = window.open(window.location.href, "_blank"); - - newWindow.onload = function () { - - // Pass the XML string to the new window using postMessage - newWindow.postMessage({ workflow: workflowXml, name: fileName }, window.location.href); - }; + const newWindow = window.open(window.location.href, "_blank"); + + newWindow.onload = function () { + // Pass the XML string to the new window using postMessage + newWindow.postMessage( + { workflow: workflowXml, name: fileName }, + window.location.href + ); + }; } -export function setAutoSaveInterval(autoSaveFileOption = editorConfig.getAutoSaveFileOption()) { - if (autoSaveFileOption === autoSaveFile.INTERVAL) { - getModeler().autosaveIntervalId = setInterval(() => { saveFile(); }, editorConfig.getAutoSaveIntervalSize()); - } else { - saveFile(); - } +export function setAutoSaveInterval( + autoSaveFileOption = editorConfig.getAutoSaveFileOption() +) { + if (autoSaveFileOption === autoSaveFile.INTERVAL) { + getModeler().autosaveIntervalId = setInterval(() => { + saveFile(); + }, editorConfig.getAutoSaveIntervalSize()); + } else { + saveFile(); + } } export function saveFile() { - // extract the xml and save it to a file - getModeler().saveXML({ format: true }, function (err, xml) { - if (!err) { - let oldXml = getModeler().oldXml; - if (oldXml !== xml && oldXml !== undefined) { - // Save the XML - console.log('Autosaved:', xml); - getModeler().oldXml = xml; - const timestamp = getTimestamp(); - const filename = `${editorConfig.getFileName().replace('.bpmn','')}_autosave_${timestamp}`; - saveXmlAsLocalFile(xml, filename); - } - } - }); + // extract the xml and save it to a file + getModeler().saveXML({ format: true }, function (err, xml) { + if (!err) { + let oldXml = getModeler().oldXml; + if (oldXml !== xml && oldXml !== undefined) { + // Save the XML + console.log("Autosaved:", xml); + getModeler().oldXml = xml; + const timestamp = getTimestamp(); + const filename = `${editorConfig + .getFileName() + .replace(".bpmn", "")}_autosave_${timestamp}`; + saveXmlAsLocalFile(xml, filename); + } + } + }); } function getTimestamp() { - const date = new Date(); - return date.toISOString().replace(/:/g, '-'); + const date = new Date(); + return date.toISOString().replace(/:/g, "-"); } export async function saveWorkflowAsSVG(modeler, fileName, fileFormat) { - modeler.saveSVG({ format: true }, function (error, svg) { - if (error) { - return; - } - - if (fileFormat === saveFileFormats.ALL || fileFormat === saveFileFormats.SVG) { - openFileDialog(svg, fileName, saveFileFormats.SVG) - } - if (fileFormat === saveFileFormats.ALL || fileFormat === saveFileFormats.PNG) { - convertSvgToPng(svg, fileName, saveFileFormats.PNG); - } - }); + modeler.saveSVG({ format: true }, function (error, svg) { + if (error) { + return; + } + + if ( + fileFormat === saveFileFormats.ALL || + fileFormat === saveFileFormats.SVG + ) { + openFileDialog(modeler, svg, fileName, saveFileFormats.SVG); + } + if ( + fileFormat === saveFileFormats.ALL || + fileFormat === saveFileFormats.PNG + ) { + convertSvgToPng(svg, fileName, saveFileFormats.PNG); + } + }); } // Function to convert SVG to PNG using an external library function convertSvgToPng(svg, fileName, fileFormat) { - var img = new Image(); - img.onload = function () { - var canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - var ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, img.width, img.height); - var pngDataUrl = canvas.toDataURL('image/png'); - downloadPng(pngDataUrl, fileName, fileFormat); - }; - img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); + var img = new Image(); + img.onload = function () { + var canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, img.width, img.height); + var pngDataUrl = canvas.toDataURL("image/png"); + downloadPng(pngDataUrl, fileName, fileFormat); + }; + img.src = + "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))); } // Function to initiate the PNG download function downloadPng(pngDataUrl, fileName, fileFormat) { - openFileUrlDialog(pngDataUrl, fileName, fileFormat); + openFileUrlDialog(pngDataUrl, fileName, fileFormat); } -async function openFileDialog(content, fileName, fileFormat) { - let suggestedName = fileName; - if (suggestedName.includes('.bpmn')) { - suggestedName = fileName.split('.bpmn')[0]; - } - let fileHandle = await window.showSaveFilePicker({ - startIn: 'downloads', suggestedName: suggestedName + fileFormat, types: [ - { - description: "BPMN file", - accept: { "text/plain": [".bpmn"] }, - }, - { - description: "SVG file", - accept: { "text/plain": [".svg"] }, - } - ] - }); - writeFile(fileHandle, content); +async function openFileDialog(modeler, content, fileName, fileFormat) { + let suggestedName = fileName; + if (suggestedName.includes(".bpmn")) { + suggestedName = fileName.split(".bpmn")[0]; + } + let fileHandle = await window.showSaveFilePicker({ + startIn: "downloads", + suggestedName: suggestedName + fileFormat, + types: [ + { + description: "BPMN file", + accept: { "text/plain": [".bpmn"] }, + }, + { + description: "SVG file", + accept: { "text/plain": [".svg"] }, + }, + ], + }); + writeFile(fileHandle, content); + if ( + fileFormat === saveFileFormats.BPMN || + fileFormat === saveFileFormats.ALL + ) { + modeler.oldXml = content; + } } async function openFileUrlDialog(content, fileName, fileFormat) { - let suggestedName = fileName; - if (suggestedName.includes('.bpmn')) { - suggestedName = fileName.split('.bpmn')[0]; - } - let fileHandle = await window.showSaveFilePicker({ - startIn: 'downloads', suggestedName: suggestedName + fileFormat, types: [ - { - description: "PNG file", - accept: { "text/plain": [".png"] }, - } - ] - }); - writeURLToFile(fileHandle, content); + let suggestedName = fileName; + if (suggestedName.includes(".bpmn")) { + suggestedName = fileName.split(".bpmn")[0]; + } + let fileHandle = await window.showSaveFilePicker({ + startIn: "downloads", + suggestedName: suggestedName + fileFormat, + types: [ + { + description: "PNG file", + accept: { "text/plain": [".png"] }, + }, + ], + }); + writeURLToFile(fileHandle, content); } async function writeFile(fileHandle, contents) { - const writable = await fileHandle.createWritable(); - await writable.write(contents); - await writable.close(); + const writable = await fileHandle.createWritable(); + await writable.write(contents); + await writable.close(); } async function writeURLToFile(fileHandle, url) { - const writable = await fileHandle.createWritable(); - const response = await fetch(url); - await response.body.pipeTo(writable); -} \ No newline at end of file + const writable = await fileHandle.createWritable(); + const response = await fetch(url); + await response.body.pipeTo(writable); +} diff --git a/components/bpmn-q/modeler-component/editor/util/ModellingUtilities.js b/components/bpmn-q/modeler-component/editor/util/ModellingUtilities.js index 07f57e2a..be056752 100644 --- a/components/bpmn-q/modeler-component/editor/util/ModellingUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/ModellingUtilities.js @@ -1,8 +1,7 @@ -import { createTempModelerFromXml } from '../ModelerHandler'; -import { getInputOutput } from './camunda-utils/InputOutputUtil'; -import { getExtension } from './camunda-utils/ExtensionElementsUtil'; -import { useService } from 'bpmn-js-properties-panel'; -import { is } from 'bpmn-js/lib/util/ModelUtil'; +import { createTempModelerFromXml } from "../ModelerHandler"; +import { getInputOutput } from "./camunda-utils/InputOutputUtil"; +import { getExtension } from "./camunda-utils/ExtensionElementsUtil"; +import { is } from "bpmn-js/lib/util/ModelUtil"; /** * Returns all start events of the workflow defined by the process businessObject @@ -11,7 +10,9 @@ import { is } from 'bpmn-js/lib/util/ModelUtil'; * @returns {*[]} All found start event elements of the workflow. */ export function getStartEvents(processBo) { - return processBo.flowElements.filter((element) => element.$type === 'bpmn:StartEvent'); + return processBo.flowElements.filter( + (element) => element.$type === "bpmn:StartEvent" + ); } /** @@ -22,28 +23,34 @@ export function getStartEvents(processBo) { * @param processVariable The process variable which should be created through the executionn listener */ export function addExecutionListener(element, moddle, processVariable) { - - // create the execution listener for the process variable - const listener = { - event: 'start', - expression: '${execution.setVariable("' + processVariable.name + '", ' + processVariable.value + ')}', - }; - - const elementBo = element.businessObject; - let extensionElements = elementBo.extensionElements; - - // create new extension element if needed - if (!extensionElements) { - extensionElements = moddle.create('bpmn:ExtensionElements'); - } - - if (!extensionElements.values) { - extensionElements.values = []; - } - - // add execution listener to the extension element of the element - extensionElements.values.push(moddle.create('camunda:ExecutionListener', listener)); - elementBo.extensionElements = extensionElements; + // create the execution listener for the process variable + const listener = { + event: "start", + expression: + '${execution.setVariable("' + + processVariable.name + + '", ' + + processVariable.value + + ")}", + }; + + const elementBo = element.businessObject; + let extensionElements = elementBo.extensionElements; + + // create new extension element if needed + if (!extensionElements) { + extensionElements = moddle.create("bpmn:ExtensionElements"); + } + + if (!extensionElements.values) { + extensionElements.values = []; + } + + // add execution listener to the extension element of the element + extensionElements.values.push( + moddle.create("camunda:ExecutionListener", listener) + ); + elementBo.extensionElements = extensionElements; } /** @@ -57,19 +64,31 @@ export function addExecutionListener(element, moddle, processVariable) { * @param moddle The moddle module of the bpmn-js modeler * @param modeling The modeling module of the bpmn-js modeler */ -export function addFormFieldForMap(elementID, name, keyValueMap, elementRegistry, moddle, modeling) { - - // create the properties of the form field - let formFieldData = - { - defaultValue: '', - id: name.replace(/\s+/g, '_'), - label: name, - type: 'string', - }; - - // create the form field for the key value map - addFormFieldDataForMap(elementID, formFieldData, keyValueMap, elementRegistry, moddle, modeling); +export function addFormFieldForMap( + elementID, + name, + keyValueMap, + elementRegistry, + moddle, + modeling +) { + // create the properties of the form field + let formFieldData = { + defaultValue: "", + id: name.replace(/\s+/g, "_"), + label: name, + type: "string", + }; + + // create the form field for the key value map + addFormFieldDataForMap( + elementID, + formFieldData, + keyValueMap, + elementRegistry, + moddle, + modeling + ); } /** @@ -83,13 +102,19 @@ export function addFormFieldForMap(elementID, name, keyValueMap, elementRegistry * @param moddle The moddle module of the bpmn-js modeler * @param modeling The modeling module of the bpmn-js modeler */ -export function addFormFieldDataForMap(elementID, formFieldData, keyValueMap, elementRegistry, moddle, modeling) { - - // create camunda properties for each entry of the key value map - formFieldData.properties = createCamundaProperties(keyValueMap, moddle); - - // create form field for form field data - addFormField(elementID, formFieldData, elementRegistry, moddle, modeling); +export function addFormFieldDataForMap( + elementID, + formFieldData, + keyValueMap, + elementRegistry, + moddle, + modeling +) { + // create camunda properties for each entry of the key value map + formFieldData.properties = createCamundaProperties(keyValueMap, moddle); + + // create form field for form field data + addFormField(elementID, formFieldData, elementRegistry, moddle, modeling); } /** @@ -101,27 +126,35 @@ export function addFormFieldDataForMap(elementID, formFieldData, keyValueMap, el * @param moddle The moddle module of the bpmn-js modeler * @param modeling The modeling module of the bpmn-js modeler */ -export function addFormField(elementID, formFieldData, elementRegistry, moddle, modeling) { - - const element = elementRegistry.get(elementID); - const extensionElements = getExtensionElements(element.businessObject, moddle); - - // get form data extension - let form = getExtension(element.businessObject, 'camunda:FormData'); - - console.log(`Found form data ${form}.`); - - if (!form) { - form = moddle.create('camunda:FormData'); - } - - // create form field - const formField = moddle.create('camunda:FormField', formFieldData); - - // save from field - pushFormField(form, formField); - extensionElements.values = [form]; - modeling.updateProperties(element, { extensionElements: extensionElements }); +export function addFormField( + elementID, + formFieldData, + elementRegistry, + moddle, + modeling +) { + const element = elementRegistry.get(elementID); + const extensionElements = getExtensionElements( + element.businessObject, + moddle + ); + + // get form data extension + let form = getExtension(element.businessObject, "camunda:FormData"); + + console.log(`Found form data ${form}.`); + + if (!form) { + form = moddle.create("camunda:FormData"); + } + + // create form field + const formField = moddle.create("camunda:FormField", formFieldData); + + // save from field + pushFormField(form, formField); + extensionElements.values = [form]; + modeling.updateProperties(element, { extensionElements: extensionElements }); } /** @@ -132,19 +165,19 @@ export function addFormField(elementID, formFieldData, elementRegistry, moddle, * @returns {bpmn:ExtensionElements} The extension elements of the businessObject */ export function getExtensionElements(businessObject, moddle) { - let extensionElements = businessObject.get('extensionElements'); + let extensionElements = businessObject.get("extensionElements"); - // create extension elements if not already defined - if (!extensionElements) { - extensionElements = moddle.create('bpmn:ExtensionElements'); - } + // create extension elements if not already defined + if (!extensionElements) { + extensionElements = moddle.create("bpmn:ExtensionElements"); + } - // init values if undefined - if (!extensionElements.values) { - extensionElements.values = []; - } + // init values if undefined + if (!extensionElements.values) { + extensionElements.values = []; + } - return extensionElements; + return extensionElements; } /** @@ -154,17 +187,18 @@ export function getExtensionElements(businessObject, moddle) { * @param formField The given Camunda form field. */ export function pushFormField(form, formField) { - - // get all fields of the form with the id of the given form field - const existingFieldsWithID = form.get('fields').filter(function (elem) { - return elem.id === formField.id; - }); - - // update existing form fields - for (let i = 0; i < existingFieldsWithID.length; i++) { - form.get('fields').splice(form.get('fields').indexOf(existingFieldsWithID[i])); - } - form.get('fields').push(formField); + // get all fields of the form with the id of the given form field + const existingFieldsWithID = form.get("fields").filter(function (elem) { + return elem.id === formField.id; + }); + + // update existing form fields + for (let i = 0; i < existingFieldsWithID.length; i++) { + form + .get("fields") + .splice(form.get("fields").indexOf(existingFieldsWithID[i])); + } + form.get("fields").push(formField); } /** @@ -174,11 +208,11 @@ export function pushFormField(form, formField) { * @returns {*} the root process element */ export function getRootProcess(definitions) { - for (let i = 0; i < definitions.rootElements.length; i++) { - if (definitions.rootElements[i].$type === 'bpmn:Process') { - return definitions.rootElements[i]; - } + for (let i = 0; i < definitions.rootElements.length; i++) { + if (definitions.rootElements[i].$type === "bpmn:Process") { + return definitions.rootElements[i]; } + } } /** @@ -188,8 +222,8 @@ export function getRootProcess(definitions) { * @return the definitions from the xml definitions */ export async function getDefinitionsFromXml(xml) { - let bpmnModeler = await createTempModelerFromXml(xml); - return bpmnModeler.getDefinitions(); + let bpmnModeler = await createTempModelerFromXml(xml); + return bpmnModeler.getDefinitions(); } /** @@ -199,12 +233,15 @@ export async function getDefinitionsFromXml(xml) { * @return the flow element if only one is defined, or undefined if none or multiple flow elements exist in the process */ export function getSingleFlowElement(process) { - let flowElements = process.flowElements; - if (flowElements.length !== 1) { - console.log('Process contains %i flow elements but must contain exactly one!', flowElements.length); - return undefined; - } - return flowElements[0]; + let flowElements = process.flowElements; + if (flowElements.length !== 1) { + console.log( + "Process contains %i flow elements but must contain exactly one!", + flowElements.length + ); + return undefined; + } + return flowElements[0]; } /** @@ -217,39 +254,47 @@ export function getSingleFlowElement(process) { * @param elementRegistry The element registry containing the elements of the current workflow * @returns {boolean} True, if element1 is connected via sequence flows with element2, false else. */ -export function findSequenceFlowConnection(element1, element2, visited, elementRegistry) { - - // exit condition of the recursion, element2 is reached - if (element1 === element2) { - return true; - } - - // store element1 as visited - visited.add(element1); - - // search recursively for element2 in all outgoing connections - const connections = element1.outgoing; - - for (let i = 0; i < connections.length; i++) { - - const connection = connections[i]; - - // only search in elements connected via sequence flow - if (connection.type === 'bpmn:SequenceFlow') { - - const nextElement = connection.target; - - // recursive call with new element - if (!visited.has(nextElement)) { - - // return true if recursive call finds element2 - if (findSequenceFlowConnection(nextElement, element2, visited, elementRegistry)) { - return true; - } - } +export function findSequenceFlowConnection( + element1, + element2, + visited, + elementRegistry +) { + // exit condition of the recursion, element2 is reached + if (element1 === element2) { + return true; + } + + // store element1 as visited + visited.add(element1); + + // search recursively for element2 in all outgoing connections + const connections = element1.outgoing; + + for (let i = 0; i < connections.length; i++) { + const connection = connections[i]; + + // only search in elements connected via sequence flow + if (connection.type === "bpmn:SequenceFlow") { + const nextElement = connection.target; + + // recursive call with new element + if (!visited.has(nextElement)) { + // return true if recursive call finds element2 + if ( + findSequenceFlowConnection( + nextElement, + element2, + visited, + elementRegistry + ) + ) { + return true; } + } } - return false; + } + return false; } /** @@ -259,49 +304,51 @@ export function findSequenceFlowConnection(element1, element2, visited, elementR * @param bpmnFactory the BPMN factory to create new BPMN elements */ export function getCamundaInputOutput(bo, bpmnFactory) { - - // retrieve InputOutput element if already defined - let inputOutput = getInputOutput(bo); - - // create new InputOutput element if non existent - if (!inputOutput || inputOutput.length === 0) { - - const extensionEntry = addEntry(bo, bo, bpmnFactory.create('camunda:InputOutput'), bpmnFactory); - - if (extensionEntry['extensionElements']) { - bo.extensionElements = extensionEntry['extensionElements']; - } else { - bo.extensionElements = extensionEntry['context']['currentObject']; - } - inputOutput = getExtension(bo, 'camunda:InputOutput'); - - if (!inputOutput) { - let inout = bpmnFactory.create('camunda:InputOutput'); - inout.inputParameters = []; - inout.outputParameters = []; - bo.extensionElements.values.push(inout); - return inout; - } else { - - // initialize parameters as empty arrays to avoid access errors - inputOutput.inputParameters = []; - inputOutput.outputParameters = []; - - // if there are multiple input/output definitions, take the first one as the modeler only uses this one - return inputOutput; - } + // retrieve InputOutput element if already defined + let inputOutput = getInputOutput(bo); + + // create new InputOutput element if non existent + if (!inputOutput || inputOutput.length === 0) { + const extensionEntry = addEntry( + bo, + bo, + bpmnFactory.create("camunda:InputOutput"), + bpmnFactory + ); + + if (extensionEntry["extensionElements"]) { + bo.extensionElements = extensionEntry["extensionElements"]; + } else { + bo.extensionElements = extensionEntry["context"]["currentObject"]; } + inputOutput = getExtension(bo, "camunda:InputOutput"); + + if (!inputOutput) { + let inout = bpmnFactory.create("camunda:InputOutput"); + inout.inputParameters = []; + inout.outputParameters = []; + bo.extensionElements.values.push(inout); + return inout; + } else { + // initialize parameters as empty arrays to avoid access errors + inputOutput.inputParameters = []; + inputOutput.outputParameters = []; - // init input/output parameters if undefined - if (!inputOutput.inputParameters) { - inputOutput.inputParameters = []; + // if there are multiple input/output definitions, take the first one as the modeler only uses this one + return inputOutput; } + } - if (!inputOutput.outputParameters) { - inputOutput.outputParameters = []; - } + // init input/output parameters if undefined + if (!inputOutput.inputParameters) { + inputOutput.inputParameters = []; + } + + if (!inputOutput.outputParameters) { + inputOutput.outputParameters = []; + } - return inputOutput; + return inputOutput; } /** @@ -313,10 +360,10 @@ export function getCamundaInputOutput(bo, bpmnFactory) { * @param bpmnFactory */ export function setInputParameter(task, name, value, bpmnFactory) { - let parameter = getInputParameter(task, name, bpmnFactory); - if (parameter) { - parameter.value = value; - } + let parameter = getInputParameter(task, name, bpmnFactory); + if (parameter) { + parameter.value = value; + } } /** @@ -328,10 +375,10 @@ export function setInputParameter(task, name, value, bpmnFactory) { * @param bpmnFactory */ export function setOutputParameter(task, name, value, bpmnFactory) { - let parameter = getOutputParameter(task, name, bpmnFactory); - if (parameter) { - parameter.value = value; - } + let parameter = getOutputParameter(task, name, bpmnFactory); + if (parameter) { + parameter.value = value; + } } /** @@ -342,15 +389,15 @@ export function setOutputParameter(task, name, value, bpmnFactory) { * @param type The given value */ export function getInputParameter(task, name, bpmnFactory) { - const extensionElement = getCamundaInputOutput(task, bpmnFactory); + const extensionElement = getCamundaInputOutput(task, bpmnFactory); - if (extensionElement && extensionElement.inputParameters) { - for (const parameter of extensionElement.inputParameters) { - if (parameter.name === name) { - return parameter; - } - } + if (extensionElement && extensionElement.inputParameters) { + for (const parameter of extensionElement.inputParameters) { + if (parameter.name === name) { + return parameter; + } } + } } /** @@ -361,15 +408,15 @@ export function getInputParameter(task, name, bpmnFactory) { * @param bpmnFactory */ export function getOutputParameter(task, name, bpmnFactory) { - const extensionElement = getCamundaInputOutput(task, bpmnFactory); + const extensionElement = getCamundaInputOutput(task, bpmnFactory); - if (extensionElement && extensionElement.outputParameters) { - for (const parameter of extensionElement.outputParameters) { - if (parameter.name === name) { - return parameter; - } - } + if (extensionElement && extensionElement.outputParameters) { + for (const parameter of extensionElement.outputParameters) { + if (parameter.name === name) { + return parameter; + } } + } } /** @@ -380,16 +427,25 @@ export function getOutputParameter(task, name, bpmnFactory) { * @param value Value of the input parameter. * @param bpmnFactory The bpmnFactory of the bpmn-js modeler. */ -export function addCamundaInputParameter(businessObject, name, value, bpmnFactory) { - - // get camunda io extension element - const inputOutputExtensions = getCamundaInputOutput(businessObject, bpmnFactory); - - // add new input parameter - inputOutputExtensions.inputParameters.push(bpmnFactory.create('camunda:InputParameter', { - name: name, - value: value, - })); +export function addCamundaInputParameter( + businessObject, + name, + value, + bpmnFactory +) { + // get camunda io extension element + const inputOutputExtensions = getCamundaInputOutput( + businessObject, + bpmnFactory + ); + + // add new input parameter + inputOutputExtensions.inputParameters.push( + bpmnFactory.create("camunda:InputParameter", { + name: name, + value: value, + }) + ); } /** @@ -400,16 +456,25 @@ export function addCamundaInputParameter(businessObject, name, value, bpmnFactor * @param value Value of the output parameter. * @param bpmnFactory The bpmnFactory of the bpmn-js modeler. */ -export function addCamundaOutputParameter(businessObject, name, value, bpmnFactory) { - - // get camunda io extension element - const inputOutputExtensions = getCamundaInputOutput(businessObject, bpmnFactory); - - // add new output parameter - inputOutputExtensions.outputParameters.push(bpmnFactory.create('camunda:OutputParameter', { - name: name, - value: value, - })); +export function addCamundaOutputParameter( + businessObject, + name, + value, + bpmnFactory +) { + // get camunda io extension element + const inputOutputExtensions = getCamundaInputOutput( + businessObject, + bpmnFactory + ); + + // add new output parameter + inputOutputExtensions.outputParameters.push( + bpmnFactory.create("camunda:OutputParameter", { + name: name, + value: value, + }) + ); } /** @@ -420,22 +485,29 @@ export function addCamundaOutputParameter(businessObject, name, value, bpmnFacto * @param keyValueMap key value map of the input parameter. * @param bpmnFactory The bpmnFactory of the bpmn-js modeler. */ -export function addCamundaInputMapParameter(businessObject, name, keyValueMap, bpmnFactory) { - - // get camunda io extension element - const inputOutputExtensions = getCamundaInputOutput(businessObject, bpmnFactory); - - // create a camunda map element for the key value map - const map = createCamundaMap(keyValueMap, bpmnFactory); - - // add the created map as new input parameter - const input = bpmnFactory.create('camunda:InputParameter', { - name: name, - definition: map, - }); - - map.$parent = input; - inputOutputExtensions.inputParameters.push(input); +export function addCamundaInputMapParameter( + businessObject, + name, + keyValueMap, + bpmnFactory +) { + // get camunda io extension element + const inputOutputExtensions = getCamundaInputOutput( + businessObject, + bpmnFactory + ); + + // create a camunda map element for the key value map + const map = createCamundaMap(keyValueMap, bpmnFactory); + + // add the created map as new input parameter + const input = bpmnFactory.create("camunda:InputParameter", { + name: name, + definition: map, + }); + + map.$parent = input; + inputOutputExtensions.inputParameters.push(input); } /** @@ -446,22 +518,29 @@ export function addCamundaInputMapParameter(businessObject, name, keyValueMap, b * @param keyValueMap key value map of the output parameter. * @param bpmnFactory The bpmnFactory of the bpmn-js modeler. */ -export function addCamundaOutputMapParameter(businessObject, name, keyValueMap, bpmnFactory) { - - // get camunda io extension element - const inputOutputExtensions = getCamundaInputOutput(businessObject, bpmnFactory); - - // create a camunda map element for the key value map - const map = createCamundaMap(keyValueMap, bpmnFactory); - - // add the created map as new output parameter - const output = bpmnFactory.create('camunda:OutputParameter', { - name: name, - definition: map, - }); - - map.$parent = output; - inputOutputExtensions.outputParameters.push(output); +export function addCamundaOutputMapParameter( + businessObject, + name, + keyValueMap, + bpmnFactory +) { + // get camunda io extension element + const inputOutputExtensions = getCamundaInputOutput( + businessObject, + bpmnFactory + ); + + // create a camunda map element for the key value map + const map = createCamundaMap(keyValueMap, bpmnFactory); + + // add the created map as new output parameter + const output = bpmnFactory.create("camunda:OutputParameter", { + name: name, + definition: map, + }); + + map.$parent = output; + inputOutputExtensions.outputParameters.push(output); } /** @@ -472,25 +551,24 @@ export function addCamundaOutputMapParameter(businessObject, name, keyValueMap, * @returns {camunda:Map} The created camunda map element */ export function createCamundaMap(keyValueMap, bpmnFactory) { - - // create camunda entry elements for the key value entries - const mapEntries = keyValueMap.map(function ({ name, value }) { - return bpmnFactory.create('camunda:Entry', { - key: name, - value: value, - }); + // create camunda entry elements for the key value entries + const mapEntries = keyValueMap.map(function ({ name, value }) { + return bpmnFactory.create("camunda:Entry", { + key: name, + value: value, }); + }); - // create the camunda map for the entries - const map = bpmnFactory.create('camunda:Map', { - entries: mapEntries, - }); + // create the camunda map for the entries + const map = bpmnFactory.create("camunda:Map", { + entries: mapEntries, + }); - for (let entry of mapEntries) { - entry.$parent = map; - } + for (let entry of mapEntries) { + entry.$parent = map; + } - return map; + return map; } /** @@ -502,25 +580,24 @@ export function createCamundaMap(keyValueMap, bpmnFactory) { * @returns {camunda:Properties} The camunda properties element */ export function createCamundaProperties(keyValueMap, moddle) { - - // create camunda property elements for each map entry - const mapEntries = keyValueMap.map(function ({ name, value }) { - return moddle.create('camunda:Property', { - id: name, - value: value, - }); + // create camunda property elements for each map entry + const mapEntries = keyValueMap.map(function ({ name, value }) { + return moddle.create("camunda:Property", { + id: name, + value: value, }); + }); - // create camunda properties element containing the created property elements - const map = moddle.create('camunda:Properties', { - values: mapEntries, - }); + // create camunda properties element containing the created property elements + const map = moddle.create("camunda:Properties", { + values: mapEntries, + }); - for (let entry of mapEntries) { - entry.$parent = map; - } + for (let entry of mapEntries) { + entry.$parent = map; + } - return map; + return map; } /** @@ -531,7 +608,7 @@ export function createCamundaProperties(keyValueMap, moddle) { * @return true if the given element is a flow like element, false otherwise */ export function isFlowLikeElement(type) { - return type === 'bpmn:SequenceFlow' || type === 'bpmn:Association'; + return type === "bpmn:SequenceFlow" || type === "bpmn:Association"; } /** @@ -541,20 +618,22 @@ export function isFlowLikeElement(type) { * @return the list of flow elements */ export function getFlowElementsRecursively(startElement) { - let flowElements = []; - - if (startElement.flowElements !== undefined) { - for (let i = 0; i < startElement.flowElements.length; i++) { - let flowElement = startElement.flowElements[i]; - - if (flowElement.$type === 'bpmn:SubProcess') { - flowElements = flowElements.concat(getFlowElementsRecursively(flowElement)); - } else { - flowElements.push(flowElement); - } - } + let flowElements = []; + + if (startElement.flowElements !== undefined) { + for (let i = 0; i < startElement.flowElements.length; i++) { + let flowElement = startElement.flowElements[i]; + + if (flowElement.$type === "bpmn:SubProcess") { + flowElements = flowElements.concat( + getFlowElementsRecursively(flowElement) + ); + } else { + flowElements.push(flowElement); + } } - return flowElements; + } + return flowElements; } /** @@ -564,14 +643,15 @@ export function getFlowElementsRecursively(startElement) { * @returns {string} The documentation property as a string */ export function getDocumentation(businessObject) { - - // get documentation - const documentationArray = businessObject.documentation || []; - - // convert documentation to string - return documentationArray.map(function (documentation) { - return documentation.text; - }).join('\n'); + // get documentation + const documentationArray = businessObject.documentation || []; + + // convert documentation to string + return documentationArray + .map(function (documentation) { + return documentation.text; + }) + .join("\n"); } /** @@ -582,9 +662,11 @@ export function getDocumentation(businessObject) { * @param bpmnFactory The bpmnFactory of the bpmn-js modeler */ export function setDocumentation(element, newDocumentation, bpmnFactory) { - element.businessObject.documentation = [bpmnFactory.create('bpmn:Documentation', { - text: newDocumentation, - })]; + element.businessObject.documentation = [ + bpmnFactory.create("bpmn:Documentation", { + text: newDocumentation, + }), + ]; } /** @@ -597,20 +679,25 @@ export function setDocumentation(element, newDocumentation, bpmnFactory) { * @returns {{extensionElements: elementType}} The updated extension elements */ export function addEntry(businessObject, element, entry, bpmnFactory) { - let extensionElements = businessObject.get('extensionElements'); - - // if there is no extensionElements list, create one - if (!extensionElements) { - extensionElements = createElement('bpmn:ExtensionElements', { values: [entry] }, businessObject, bpmnFactory); - return { extensionElements: extensionElements }; - } - - // add extension element to list if it exists - entry.$parent = extensionElements; - let values = extensionElements.get('values'); - values.push(entry); - extensionElements.set('values', values); + let extensionElements = businessObject.get("extensionElements"); + + // if there is no extensionElements list, create one + if (!extensionElements) { + extensionElements = createElement( + "bpmn:ExtensionElements", + { values: [entry] }, + businessObject, + bpmnFactory + ); return { extensionElements: extensionElements }; + } + + // add extension element to list if it exists + entry.$parent = extensionElements; + let values = extensionElements.get("values"); + values.push(entry); + extensionElements.set("values", values); + return { extensionElements: extensionElements }; } /** @@ -623,10 +710,10 @@ export function addEntry(businessObject, element, entry, bpmnFactory) { * @returns {elementType} The created element */ export function createElement(elementType, properties, parent, factory) { - let element = factory.create(elementType, properties); - element.$parent = parent; + let element = factory.create(elementType, properties); + element.$parent = parent; - return element; + return element; } /** @@ -641,21 +728,28 @@ export function createElement(elementType, properties, parent, factory) { * @param autoPlace The create module of the bpmn-js modeler * @returns {Shape} The new created diagram element */ -export function appendElement(type, element, event, bpmnFactory, elementFactory, create, autoPlace) { - - const businessObject = bpmnFactory.create(type); - const shape = elementFactory.createShape({ - type: type, - businessObject: businessObject - }); - - if (autoPlace) { - autoPlace.append(element, shape); - } else { - create.start(event, shape); - } - - return shape; +export function appendElement( + type, + element, + event, + bpmnFactory, + elementFactory, + create, + autoPlace +) { + const businessObject = bpmnFactory.create(type); + const shape = elementFactory.createShape({ + type: type, + businessObject: businessObject, + }); + + if (autoPlace) { + autoPlace.append(element, shape); + } else { + create.start(event, shape); + } + + return shape; } /** @@ -665,12 +759,19 @@ export function appendElement(type, element, event, bpmnFactory, elementFactory, * @param replacementType The type of the new connection. * @param modeling The modeling module of the bpmn-js modeler. */ -export function replaceConnection(connectionElement, replacementType, modeling) { - const sourceElement = connectionElement.source; - const targetElement = connectionElement.target; - - modeling.removeConnection(connectionElement); - modeling.connect(sourceElement, targetElement, { type: replacementType, waypoints: connectionElement.waypoints }); +export function replaceConnection( + connectionElement, + replacementType, + modeling +) { + const sourceElement = connectionElement.source; + const targetElement = connectionElement.target; + + modeling.removeConnection(connectionElement); + modeling.connect(sourceElement, targetElement, { + type: replacementType, + waypoints: connectionElement.waypoints, + }); } /** @@ -681,16 +782,19 @@ export function replaceConnection(connectionElement, replacementType, modeling) * @returns {boolean} True if the given element is connected with an element of the given type, false else. */ export function isConnectedWith(element, connectedElementType) { - - const outgoingConnections = element.outgoing || []; - const incomingConnections = element.incoming || []; - - // check if a source or target of a connection is of the given type - for (let connectedElement of outgoingConnections.concat(incomingConnections)) { - if (is(connectedElement.source, connectedElementType) || is(connectedElement.target, connectedElementType)) { - return true; - } + const outgoingConnections = element.outgoing || []; + const incomingConnections = element.incoming || []; + + // check if a source or target of a connection is of the given type + for (let connectedElement of outgoingConnections.concat( + incomingConnections + )) { + if ( + is(connectedElement.source, connectedElementType) || + is(connectedElement.target, connectedElementType) + ) { + return true; } - return false; + } + return false; } - diff --git a/components/bpmn-q/modeler-component/editor/util/PopupMenuUtilities.js b/components/bpmn-q/modeler-component/editor/util/PopupMenuUtilities.js index 28b44753..d707e6cc 100644 --- a/components/bpmn-q/modeler-component/editor/util/PopupMenuUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/PopupMenuUtilities.js @@ -11,29 +11,35 @@ * @param customStyleClass The style class of the MoreOptionsEntry. * @returns {{label: string, className: string, action: Function}} The created MoreOptionsEntry */ -export function createMoreOptionsEntryWithReturn(originalElement, title, entryName, popupMenu, options, customStyleClass) { - - const lessOptionsEntry = createLessOptionsEntry( - originalElement, - 'Change Element', - 'All Entries', - popupMenu, - undefined, - ); - - // entries of the new popup menu - let entries = {}; - entries['replace-by-less-options'] = lessOptionsEntry; - entries = Object.assign(entries, options); - - return createMoreOptionsEntry( - title, - title, - entryName, - popupMenu, - entries, - customStyleClass, - ); +export function createMoreOptionsEntryWithReturn( + originalElement, + title, + entryName, + popupMenu, + options, + customStyleClass +) { + const lessOptionsEntry = createLessOptionsEntry( + originalElement, + "Change Element", + "All Entries", + popupMenu, + undefined + ); + + // entries of the new popup menu + let entries = {}; + entries["replace-by-less-options"] = lessOptionsEntry; + entries = Object.assign(entries, options); + + return createMoreOptionsEntry( + title, + title, + entryName, + popupMenu, + entries, + customStyleClass + ); } /** @@ -55,27 +61,37 @@ export function createMoreOptionsEntryWithReturn(originalElement, title, entryNa * @returns {{ label: string, className: string, action: function}}: The popup menu entry which shows another popup menu * when clicked. */ -export function createMoreOptionsEntry(optionsType, title, entryName, popupMenu, entries, customStyleClass) { - - // add customStyleClass to the default classname if set - const classname = customStyleClass ? 'qwm-popup-menu-more-options ' + customStyleClass : 'popup-menu-more-options'; - - // create a popup menu entry which triggers a new popup menu for the optionsType - return { - label: entryName, - className: classname, - moreOptions: entries, - action: function () { - - popupMenu.openWithEntries({type: optionsType}, "bpmn-replace", entries, - { - title: title, - width: 300, - search: true, - } - ); +export function createMoreOptionsEntry( + optionsType, + title, + entryName, + popupMenu, + entries, + customStyleClass +) { + // add customStyleClass to the default classname if set + const classname = customStyleClass + ? "qwm-popup-menu-more-options " + customStyleClass + : "popup-menu-more-options"; + + // create a popup menu entry which triggers a new popup menu for the optionsType + return { + label: entryName, + className: classname, + moreOptions: entries, + action: function () { + popupMenu.openWithEntries( + { type: optionsType }, + "bpmn-replace", + entries, + { + title: title, + width: 300, + search: true, } - }; + ); + }, + }; } /** @@ -91,22 +107,25 @@ export function createMoreOptionsEntry(optionsType, title, entryName, popupMenu, * @param entries The entries of the new popup menu (optional) * @returns {{action: action, className: string, label}} The created LessOptionsEntry */ -export function createLessOptionsEntry(originalElement, title, entryName, popupMenu, entries) { - - // create a popup menu entry which triggers a new popup menu for the optionsType - return { - label: entryName, - className: 'qwm-popup-menu-less-options', - action: function () { - popupMenu.openWithEntries(originalElement, "bpmn-replace", entries, - { - title: title, - width: 300, - search: true, - } - ); - } - }; +export function createLessOptionsEntry( + originalElement, + title, + entryName, + popupMenu, + entries +) { + // create a popup menu entry which triggers a new popup menu for the optionsType + return { + label: entryName, + className: "qwm-popup-menu-less-options", + action: function () { + popupMenu.openWithEntries(originalElement, "bpmn-replace", entries, { + title: title, + width: 300, + search: true, + }); + }, + }; } /** @@ -118,17 +137,26 @@ export function createLessOptionsEntry(originalElement, title, entryName, popupM * @param replaceElement The replaceElement function of the bpmn-js modeler. * @returns {{}} Object containing the created menu entries. */ -export function createMenuEntries(element, definitions, translate, replaceElement) { - - let menuEntries = {}; - let id; - - // create menu entries for each entry in the definitions - for (let definition of definitions) { - id = definition.id || definition.actionName; - menuEntries[id] = createMenuEntry(element, definition, translate, replaceElement); - } - return menuEntries; +export function createMenuEntries( + element, + definitions, + translate, + replaceElement +) { + let menuEntries = {}; + let id; + + // create menu entries for each entry in the definitions + for (let definition of definitions) { + id = definition.id || definition.actionName; + menuEntries[id] = createMenuEntry( + element, + definition, + translate, + replaceElement + ); + } + return menuEntries; } /** @@ -141,23 +169,28 @@ export function createMenuEntries(element, definitions, translate, replaceElemen * @param action The action which is triggered when the menu entry is selected, if undefined the replaceAction is used. * @returns {{action: (function(): *), className, label}} The created menu entry. */ -export function createMenuEntry(element, definition, translate, replaceElement, action = undefined) { - - // replace the element by the element type defined in definition.target - const replaceAction = function () { - console.log(definition.target); - return replaceElement(element, definition.target); - }; - - const label = definition.label || ''; - - action = action || replaceAction; - - return { - label: translate(label), - className: definition.className, - action: action - }; +export function createMenuEntry( + element, + definition, + translate, + replaceElement, + action = undefined +) { + // replace the element by the element type defined in definition.target + const replaceAction = function () { + console.log(definition.target); + return replaceElement(element, definition.target); + }; + + const label = definition.label || ""; + + action = action || replaceAction; + + return { + label: translate(label), + className: definition.className, + action: action, + }; } /** @@ -167,16 +200,17 @@ export function createMenuEntry(element, definition, translate, replaceElement, * @returns {*|unknown[]} List of menu entries or a single entry if the given entry is not a MoreOptionsEntry */ export function getMoreOptions(entry) { - if (entry.moreOptions) { - - // skip first entry because this is the entry for returning to the original menu - return Object.entries(entry.moreOptions).slice(1).flatMap(function ([key, value]) { - value.id = key; - - // recursively resolve each menu entry - return getMoreOptions(value); - }); - } else { - return entry; - } -} \ No newline at end of file + if (entry.moreOptions) { + // skip first entry because this is the entry for returning to the original menu + return Object.entries(entry.moreOptions) + .slice(1) + .flatMap(function ([key, value]) { + value.id = key; + + // recursively resolve each menu entry + return getMoreOptions(value); + }); + } else { + return entry; + } +} diff --git a/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js b/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js index 5395db0b..67ac4b50 100644 --- a/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/RenderUtilities.js @@ -1,10 +1,10 @@ import { - append as svgAppend, - attr as svgAttr, - create as svgCreate, - innerSVG, - select as svgSelect -} from 'tiny-svg'; + append as svgAppend, + attr as svgAttr, + create as svgCreate, + innerSVG, + select as svgSelect, +} from "tiny-svg"; /** * Draw svg path with the given attributes. @@ -15,44 +15,13 @@ import { * @returns {SVGPathElement} */ export function drawPath(parentGfx, d, attrs) { + const path = svgCreate("path"); + svgAttr(path, { d: d }); + svgAttr(path, attrs); - const path = svgCreate('path'); - svgAttr(path, {d: d}); - svgAttr(path, attrs); + svgAppend(parentGfx, path); - svgAppend(parentGfx, path); - - return path; -} - -/** - * Draw a SVG rectangle with the given width, height, border radius and color - * - * Copied from https://github.com/bpmn-io/bpmn-js/blob/master/lib/draw/BpmnRenderer.js - * - * @param parentNode The parent element the svg rectangle is appended to. - * @param width The given width - * @param height The given height - * @param borderRadius The given border radius - * @param color The given color - * @returns {SVGRectElement} - */ -function drawRect(parentNode, width, height, borderRadius, color) { - const rect = svgCreate('rect'); - - svgAttr(rect, { - width: width, - height: height, - rx: borderRadius, - ry: borderRadius, - stroke: color, - strokeWidth: 2, - fill: color - }); - - svgAppend(parentNode, rect); - - return rect; + return path; } /** @@ -65,28 +34,28 @@ function drawRect(parentNode, width, height, borderRadius, color) { * @returns The created svg element */ export function drawTaskSVG(parentGfx, importSVG, svgAttributes, foreground) { - const innerSvgStr = importSVG.svg, - transformDef = importSVG.transform; + const innerSvgStr = importSVG.svg, + transformDef = importSVG.transform; - const groupDef = svgCreate('g'); - svgAttr(groupDef, {transform: transformDef}); - innerSVG(groupDef, innerSvgStr); + const groupDef = svgCreate("g"); + svgAttr(groupDef, { transform: transformDef }); + innerSVG(groupDef, innerSvgStr); - if(!foreground) { - // set task box opacity to 0 such that icon can be in the background - svgAttr(svgSelect(parentGfx, 'rect'), { 'fill-opacity': 0 }); - } + if (!foreground) { + // set task box opacity to 0 such that icon can be in the background + svgAttr(svgSelect(parentGfx, "rect"), { "fill-opacity": 0 }); + } - if (svgAttributes) { - svgAttr(groupDef, svgAttributes); - } + if (svgAttributes) { + svgAttr(groupDef, svgAttributes); + } - if(foreground) { - parentGfx.append(groupDef); - } else { - parentGfx.prepend(groupDef); - } - return groupDef; + if (foreground) { + parentGfx.append(groupDef); + } else { + parentGfx.prepend(groupDef); + } + return groupDef; } /** @@ -97,16 +66,16 @@ export function drawTaskSVG(parentGfx, importSVG, svgAttributes, foreground) { * @param svgAttributes Attributes for the SVG */ export function drawDataElementSVG(parentGfx, importSVG, svgAttributes) { - const innerSvgStr = importSVG.svg, - transformDef = importSVG.transform; + const innerSvgStr = importSVG.svg, + transformDef = importSVG.transform; - const groupDef = svgCreate('g'); - svgAttr(groupDef, {transform: transformDef}); - innerSVG(groupDef, innerSvgStr); + const groupDef = svgCreate("g"); + svgAttr(groupDef, { transform: transformDef }); + innerSVG(groupDef, innerSvgStr); - if (svgAttributes) { - svgAttr(groupDef, svgAttributes); - } + if (svgAttributes) { + svgAttr(groupDef, svgAttributes); + } - parentGfx.append(groupDef); -} \ No newline at end of file + parentGfx.append(groupDef); +} diff --git a/components/bpmn-q/modeler-component/editor/util/TransformationUtilities.js b/components/bpmn-q/modeler-component/editor/util/TransformationUtilities.js index 000a5b8e..6ee8f156 100644 --- a/components/bpmn-q/modeler-component/editor/util/TransformationUtilities.js +++ b/components/bpmn-q/modeler-component/editor/util/TransformationUtilities.js @@ -1,5 +1,5 @@ -import { isFlowLikeElement } from './ModellingUtilities'; -import { getDi, is } from 'bpmn-js/lib/util/ModelUtil'; +import { isFlowLikeElement } from "./ModellingUtilities"; +import { getDi, is } from "bpmn-js/lib/util/ModelUtil"; /** * Insert the given element and all child elements into the diagram @@ -13,101 +13,135 @@ import { getDi, is } from 'bpmn-js/lib/util/ModelUtil'; * @param oldElement an old element that is only required if it should be replaced by the new element * @return {{success: boolean, idMap: *, element: *}} */ -export function insertShape(definitions, parent, newElement, idMap, replace, modeler, oldElement) { - console.log('Inserting shape for element: ', newElement); - let bpmnReplace = modeler.get('bpmnReplace'); - let bpmnFactory = modeler.get('bpmnFactory'); - let modeling = modeler.get('modeling'); - let elementRegistry = modeler.get('elementRegistry'); - - // create new id map if not provided - if (idMap === undefined) { - idMap = {}; - } - - let element; - if (!isFlowLikeElement(newElement.$type)) { - if (replace) { - - // replace old element to retain attached sequence flow, associations, data objects, ... - element = bpmnReplace.replaceElement(elementRegistry.get(oldElement.id), { type: newElement.$type }); - } else { - - // create new shape for this element - element = modeling.createShape({ type: newElement.$type }, { x: 50, y: 50 }, parent, {}); - } +export function insertShape( + definitions, + parent, + newElement, + idMap, + replace, + modeler, + oldElement +) { + console.log("Inserting shape for element: ", newElement); + let bpmnReplace = modeler.get("bpmnReplace"); + let bpmnFactory = modeler.get("bpmnFactory"); + let modeling = modeler.get("modeling"); + let elementRegistry = modeler.get("elementRegistry"); + + // create new id map if not provided + if (idMap === undefined) { + idMap = {}; + } + + let element; + if (!isFlowLikeElement(newElement.$type)) { + if (replace) { + // replace old element to retain attached sequence flow, associations, data objects, ... + element = bpmnReplace.replaceElement(elementRegistry.get(oldElement.id), { + type: newElement.$type, + }); } else { - - // create connection between two previously created elements - let sourceElement = elementRegistry.get(idMap[newElement.sourceRef.id]); - let targetElement = elementRegistry.get(idMap[newElement.targetRef.id]); - element = modeling.connect(sourceElement, targetElement, { type: newElement.$type }); + // create new shape for this element + element = modeling.createShape( + { type: newElement.$type }, + { x: 50, y: 50 }, + parent, + {} + ); } - - // store id to create sequence flows - idMap[newElement['id']] = element.id; - - // if the element is a subprocess, check if it is expanded in the replacement fragment and expand the new element - if (['bpmn:SubProcess', 'quantme:QuantumHardwareSelectionSubprocess', 'quantme:CircuitCuttingSubprocess'].includes(newElement.$type)) { - - // get the shape element related to the subprocess - let shape = getDi(element); - - // expand the replacement subprocess if the detector subprocess was expanded - if (shape && (newElement.isExpanded === 'true')) { - shape.isExpanded = true; - } - - // preserve messages defined in ReceiveTasks - } else if (newElement.$type === 'bpmn:ReceiveTask' && newElement.messageRef) { - - // get message from the replacement and check if a corresponding message was already created - let oldMessage = newElement.messageRef; - if (idMap[oldMessage.id] === undefined) { - - // add a new message element to the definitions document and link it to the receive task - let message = bpmnFactory.create('bpmn:Message'); - message.name = oldMessage.name; - definitions.rootElements.push(message); - modeling.updateProperties(element, { 'messageRef': message }); - - // store id if other receive tasks reference the same message - idMap[oldMessage.id] = message.id; - } else { - - // reuse already created message and add it to receive task - modeling.updateProperties(element, { 'messageRef': idMap[oldMessage.id] }); - } + } else { + // create connection between two previously created elements + let sourceElement = elementRegistry.get(idMap[newElement.sourceRef.id]); + let targetElement = elementRegistry.get(idMap[newElement.targetRef.id]); + element = modeling.connect(sourceElement, targetElement, { + type: newElement.$type, + }); + } + + // store id to create sequence flows + idMap[newElement["id"]] = element.id; + + // if the element is a subprocess, check if it is expanded in the replacement fragment and expand the new element + if ( + [ + "bpmn:SubProcess", + "quantme:QuantumHardwareSelectionSubprocess", + "quantme:CircuitCuttingSubprocess", + ].includes(newElement.$type) + ) { + // get the shape element related to the subprocess + let shape = getDi(element); + + // expand the replacement subprocess if the detector subprocess was expanded + if (shape && newElement.isExpanded === "true") { + shape.isExpanded = true; } - // add element to which a boundary event is attached - if (newElement.$type === 'bpmn:BoundaryEvent') { - let hostElement = elementRegistry.get(idMap[newElement.attachedToRef.id]); - modeling.updateProperties(element, { 'attachedToRef': hostElement.businessObject }); - element.host = hostElement; + // preserve messages defined in ReceiveTasks + } else if (newElement.$type === "bpmn:ReceiveTask" && newElement.messageRef) { + // get message from the replacement and check if a corresponding message was already created + let oldMessage = newElement.messageRef; + if (idMap[oldMessage.id] === undefined) { + // add a new message element to the definitions document and link it to the receive task + let message = bpmnFactory.create("bpmn:Message"); + message.name = oldMessage.name; + definitions.rootElements.push(message); + modeling.updateProperties(element, { messageRef: message }); + + // store id if other receive tasks reference the same message + idMap[oldMessage.id] = message.id; + } else { + // reuse already created message and add it to receive task + modeling.updateProperties(element, { messageRef: idMap[oldMessage.id] }); } - - // update the properties of the new element - modeling.updateProperties(element, getPropertiesToCopy(newElement)); - - // recursively handle children of the current element - let resultTuple = insertChildElements(definitions, element, newElement, idMap, modeler); - - // add artifacts with their shapes to the diagram - let success = resultTuple['success']; - idMap = resultTuple['idMap']; - let artifacts = newElement.artifacts; - if (artifacts) { - console.log('Element contains %i artifacts. Adding corresponding shapes...', artifacts.length); - for (let i = 0; i < artifacts.length; i++) { - let result = insertShape(definitions, element, artifacts[i], idMap, false, modeler); - success = success && result['success']; - idMap = result['idMap']; - } + } + + // add element to which a boundary event is attached + if (newElement.$type === "bpmn:BoundaryEvent") { + let hostElement = elementRegistry.get(idMap[newElement.attachedToRef.id]); + modeling.updateProperties(element, { + attachedToRef: hostElement.businessObject, + }); + element.host = hostElement; + } + + // update the properties of the new element + modeling.updateProperties(element, getPropertiesToCopy(newElement)); + + // recursively handle children of the current element + let resultTuple = insertChildElements( + definitions, + element, + newElement, + idMap, + modeler + ); + + // add artifacts with their shapes to the diagram + let success = resultTuple["success"]; + idMap = resultTuple["idMap"]; + let artifacts = newElement.artifacts; + if (artifacts) { + console.log( + "Element contains %i artifacts. Adding corresponding shapes...", + artifacts.length + ); + for (let i = 0; i < artifacts.length; i++) { + let result = insertShape( + definitions, + element, + artifacts[i], + idMap, + false, + modeler + ); + success = success && result["success"]; + idMap = result["idMap"]; } + } - // return success flag and idMap with id mappings of this element and all children - return { success: success, idMap: idMap, element: element }; + // return success flag and idMap with id mappings of this element and all children + return { success: success, idMap: idMap, element: element }; } /** @@ -120,47 +154,75 @@ export function insertShape(definitions, parent, newElement, idMap, replace, mod * @param modeler the BPMN modeler containing the target BPMN diagram * @return {{success: boolean, idMap: *, element: *}} */ -export function insertChildElements(definitions, parent, newElement, idMap, modeler) { - - let success = true; - let flowElements = newElement.flowElements; - let boundaryEvents = []; - let sequenceflows = []; - if (flowElements) { - console.log('Element contains %i children. Adding corresponding shapes...', flowElements.length); - for (let i = 0; i < flowElements.length; i++) { - - // skip elements with references and add them after all other elements to set correct references - if (flowElements[i].$type === 'bpmn:SequenceFlow') { - sequenceflows.push(flowElements[i]); - continue; - } - if (flowElements[i].$type === 'bpmn:BoundaryEvent') { - boundaryEvents.push(flowElements[i]); - continue; - } - - let result = insertShape(definitions, parent, flowElements[i], idMap, false, modeler); - success = success && result['success']; - idMap = result['idMap']; - } +export function insertChildElements( + definitions, + parent, + newElement, + idMap, + modeler +) { + let success = true; + let flowElements = newElement.flowElements; + let boundaryEvents = []; + let sequenceflows = []; + if (flowElements) { + console.log( + "Element contains %i children. Adding corresponding shapes...", + flowElements.length + ); + for (let i = 0; i < flowElements.length; i++) { + // skip elements with references and add them after all other elements to set correct references + if (flowElements[i].$type === "bpmn:SequenceFlow") { + sequenceflows.push(flowElements[i]); + continue; + } + if (flowElements[i].$type === "bpmn:BoundaryEvent") { + boundaryEvents.push(flowElements[i]); + continue; + } + + let result = insertShape( + definitions, + parent, + flowElements[i], + idMap, + false, + modeler + ); + success = success && result["success"]; + idMap = result["idMap"]; + } - // handle boundary events with new ids of added elements - for (let i = 0; i < boundaryEvents.length; i++) { - let result = insertShape(definitions, parent, boundaryEvents[i], idMap, false, modeler); - success = success && result['success']; - idMap = result['idMap']; - } + // handle boundary events with new ids of added elements + for (let i = 0; i < boundaryEvents.length; i++) { + let result = insertShape( + definitions, + parent, + boundaryEvents[i], + idMap, + false, + modeler + ); + success = success && result["success"]; + idMap = result["idMap"]; + } - // handle boundary events with new ids of added elements - for (let i = 0; i < sequenceflows.length; i++) { - let result = insertShape(definitions, parent, sequenceflows[i], idMap, false, modeler); - success = success && result['success']; - idMap = result['idMap']; - } + // handle boundary events with new ids of added elements + for (let i = 0; i < sequenceflows.length; i++) { + let result = insertShape( + definitions, + parent, + sequenceflows[i], + idMap, + false, + modeler + ); + success = success && result["success"]; + idMap = result["idMap"]; } + } - return { success: success, idMap: idMap, element: parent }; + return { success: success, idMap: idMap, element: parent }; } /** @@ -170,43 +232,42 @@ export function insertChildElements(definitions, parent, newElement, idMap, mode * @return the properties to copy */ export function getPropertiesToCopy(element) { - let properties = {}; - for (let key in element) { - - // ignore properties from parent element - if (!element.hasOwnProperty(key)) { - continue; - } - - // ignore properties such as type - if (key.startsWith('$')) { - continue; - } + let properties = {}; + for (let key in element) { + // ignore properties from parent element + if (!element.hasOwnProperty(key)) { + continue; + } - // ignore id as it is automatically generated with the shape - if (key === 'id') { - continue; - } + // ignore properties such as type + if (key.startsWith("$")) { + continue; + } - // ignore flow elements, as the children are added afterwards - if (key === 'flowElements') { - continue; - } + // ignore id as it is automatically generated with the shape + if (key === "id") { + continue; + } - // ignore artifacts, as they are added afterwards with their shapes - if (key === 'artifacts') { - continue; - } + // ignore flow elements, as the children are added afterwards + if (key === "flowElements") { + continue; + } - // ignore messages, as they are added before - if (key === 'messageRef') { - continue; - } + // ignore artifacts, as they are added afterwards with their shapes + if (key === "artifacts") { + continue; + } - properties[key] = element[key]; + // ignore messages, as they are added before + if (key === "messageRef") { + continue; } - return properties; + properties[key] = element[key]; + } + + return properties; } /** @@ -217,25 +278,31 @@ export function getPropertiesToCopy(element) { * @param elementType The searched element type * @returns {*[]} All elements of the process with the elementType */ -export function getAllElementsInProcess(processBo, elementRegistry, elementType) { - - // retrieve parent object for later replacement - const processElement = elementRegistry.get(processBo.id); - - const elements = []; - const flowElementBos = processBo.flowElements; - for (let i = 0; i < flowElementBos.length; i++) { - let flowElementBo = flowElementBos[i]; - if (flowElementBo.$type && flowElementBo.$type === elementType) { - elements.push({ element: flowElementBo, parent: processElement }); - } +export function getAllElementsInProcess( + processBo, + elementRegistry, + elementType +) { + // retrieve parent object for later replacement + const processElement = elementRegistry.get(processBo.id); + + const elements = []; + const flowElementBos = processBo.flowElements; + for (let i = 0; i < flowElementBos.length; i++) { + let flowElementBo = flowElementBos[i]; + if (flowElementBo.$type && flowElementBo.$type === elementType) { + elements.push({ element: flowElementBo, parent: processElement }); + } - // recursively retrieve service tasks if subprocess is found - if (flowElementBo.$type && flowElementBo.$type === 'bpmn:SubProcess') { - Array.prototype.push.apply(elements, getAllElementsInProcess(flowElementBo, elementRegistry, elementType)); - } + // recursively retrieve service tasks if subprocess is found + if (flowElementBo.$type && flowElementBo.$type === "bpmn:SubProcess") { + Array.prototype.push.apply( + elements, + getAllElementsInProcess(flowElementBo, elementRegistry, elementType) + ); } - return elements; + } + return elements; } /** @@ -246,18 +313,21 @@ export function getAllElementsInProcess(processBo, elementRegistry, elementType) * @param elementType The searched element type * @returns {*[]} All elements of the process with the elementType */ -export function getAllElementsForProcess(processBo, elementRegistry, elementType) { - - // retrieve parent object for later replacement - const processElement = elementRegistry.get(processBo.id); - - const elements = []; - const flowElements = processBo.flowElements; - for (let i = 0; i < flowElements.length; i++) { - let flowElement = flowElements[i]; - if (is(flowElement, elementType)) { - elements.push({ element: flowElement, parent: processElement }); - } +export function getAllElementsForProcess( + processBo, + elementRegistry, + elementType +) { + // retrieve parent object for later replacement + const processElement = elementRegistry.get(processBo.id); + + const elements = []; + const flowElements = processBo.flowElements; + for (let i = 0; i < flowElements.length; i++) { + let flowElement = flowElements[i]; + if (is(flowElement, elementType)) { + elements.push({ element: flowElement, parent: processElement }); } - return elements; + } + return elements; } diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ConnectorUtil.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ConnectorUtil.js index c0c238d0..09c7981d 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ConnectorUtil.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ConnectorUtil.js @@ -4,24 +4,24 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import { - getServiceTaskLikeBusinessObject -} from './ImplementationTypeUtils'; -import {getExtensionElementsList} from "./ExtensionElementsUtil"; -import {getImplementationType} from "../../../extensions/quantme/utilities/ImplementationTypeHelperExtension"; +import { getServiceTaskLikeBusinessObject } from "./ImplementationTypeUtils"; +import { getExtensionElementsList } from "./ExtensionElementsUtil"; +import { getImplementationType } from "../../../extensions/quantme/utilities/ImplementationTypeHelperExtension"; export function areConnectorsSupported(element) { - const businessObject = getServiceTaskLikeBusinessObject(element); - return businessObject && getImplementationType(businessObject) === 'connector'; + const businessObject = getServiceTaskLikeBusinessObject(element); + return ( + businessObject && getImplementationType(businessObject) === "connector" + ); } export function getConnectors(businessObject) { - return getExtensionElementsList(businessObject, 'camunda:Connector'); + return getExtensionElementsList(businessObject, "camunda:Connector"); } export function getConnector(element) { - const businessObject = getServiceTaskLikeBusinessObject(element); - const connectors = getConnectors(businessObject); + const businessObject = getServiceTaskLikeBusinessObject(element); + const connectors = getConnectors(businessObject); - return connectors[0]; -} \ No newline at end of file + return connectors[0]; +} diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ElementUtil.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ElementUtil.js index 3d356d04..42d6e04f 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ElementUtil.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ElementUtil.js @@ -4,11 +4,9 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import Ids from 'ids'; +import Ids from "ids"; -import { - is -} from 'bpmn-js/lib/util/ModelUtil'; +import { is } from "bpmn-js/lib/util/ModelUtil"; /** * Create a new element and set its parent. @@ -21,48 +19,48 @@ import { * @returns {djs.model.Base} element which is created */ export function createElement(elementType, properties, parent, factory) { - const element = factory.create(elementType, properties); + const element = factory.create(elementType, properties); - if (parent) { - element.$parent = parent; - } + if (parent) { + element.$parent = parent; + } - return element; + return element; } /** * generate a semantic id with given prefix */ export function nextId(prefix) { - const ids = new Ids([32, 32, 1]); + const ids = new Ids([32, 32, 1]); - return ids.nextPrefixed(prefix); + return ids.nextPrefixed(prefix); } export function getRoot(businessObject) { - let parent = businessObject; + let parent = businessObject; - while (parent.$parent) { - parent = parent.$parent; - } + while (parent.$parent) { + parent = parent.$parent; + } - return parent; + return parent; } export function filterElementsByType(objectList, type) { - const list = objectList || []; + const list = objectList || []; - return list.filter(element => is(element, type)); + return list.filter((element) => is(element, type)); } export function findRootElementsByType(businessObject, referencedType) { - const root = getRoot(businessObject); + const root = getRoot(businessObject); - return filterElementsByType(root.get('rootElements'), referencedType); + return filterElementsByType(root.get("rootElements"), referencedType); } export function findRootElementById(businessObject, type, id) { - const elements = findRootElementsByType(businessObject, type); + const elements = findRootElementsByType(businessObject, type); - return elements.find(element => element.id === id); + return elements.find((element) => element.id === id); } diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/EventDefinitionUtil.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/EventDefinitionUtil.js index 8711935a..460eacec 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/EventDefinitionUtil.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/EventDefinitionUtil.js @@ -4,37 +4,34 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import { - isAny -} from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; +import { isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil"; -import { - getBusinessObject, - is -} from 'bpmn-js/lib/util/ModelUtil'; +import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil"; -import { - find -} from 'min-dash'; +import { find } from "min-dash"; export function isErrorSupported(element) { - return isAny(element, [ - 'bpmn:StartEvent', - 'bpmn:BoundaryEvent', - 'bpmn:EndEvent' - ]) && !!getErrorEventDefinition(element); + return ( + isAny(element, [ + "bpmn:StartEvent", + "bpmn:BoundaryEvent", + "bpmn:EndEvent", + ]) && !!getErrorEventDefinition(element) + ); } export function getErrorEventDefinition(element) { - return getEventDefinition(element, 'bpmn:ErrorEventDefinition'); + return getEventDefinition(element, "bpmn:ErrorEventDefinition"); } export function isTimerSupported(element) { - return isAny(element, [ - 'bpmn:StartEvent', - 'bpmn:IntermediateCatchEvent', - 'bpmn:BoundaryEvent' - ]) && !!getTimerEventDefinition(element); + return ( + isAny(element, [ + "bpmn:StartEvent", + "bpmn:IntermediateCatchEvent", + "bpmn:BoundaryEvent", + ]) && !!getTimerEventDefinition(element) + ); } /** @@ -45,125 +42,132 @@ export function isTimerSupported(element) { * @return {string|undefined} the timer definition type */ export function getTimerDefinitionType(timer) { + if (!timer) { + return; + } - if (!timer) { - return; - } + const timeDate = timer.get("timeDate"); + if (typeof timeDate !== "undefined") { + return "timeDate"; + } - const timeDate = timer.get('timeDate'); - if (typeof timeDate !== 'undefined') { - return 'timeDate'; - } + const timeCycle = timer.get("timeCycle"); + if (typeof timeCycle !== "undefined") { + return "timeCycle"; + } - const timeCycle = timer.get('timeCycle'); - if (typeof timeCycle !== 'undefined') { - return 'timeCycle'; - } - - const timeDuration = timer.get('timeDuration'); - if (typeof timeDuration !== 'undefined') { - return 'timeDuration'; - } + const timeDuration = timer.get("timeDuration"); + if (typeof timeDuration !== "undefined") { + return "timeDuration"; + } } export function getTimerEventDefinition(element) { - return getEventDefinition(element, 'bpmn:TimerEventDefinition'); + return getEventDefinition(element, "bpmn:TimerEventDefinition"); } export function getError(element) { - const errorEventDefinition = getErrorEventDefinition(element); + const errorEventDefinition = getErrorEventDefinition(element); - return errorEventDefinition && errorEventDefinition.get('errorRef'); + return errorEventDefinition && errorEventDefinition.get("errorRef"); } export function getEventDefinition(element, eventType) { - const businessObject = getBusinessObject(element); + const businessObject = getBusinessObject(element); - const eventDefinitions = businessObject.get('eventDefinitions') || []; + const eventDefinitions = businessObject.get("eventDefinitions") || []; - return find(eventDefinitions, function (definition) { - return is(definition, eventType); - }); + return find(eventDefinitions, function (definition) { + return is(definition, eventType); + }); } export function isMessageSupported(element) { - return is(element, 'bpmn:ReceiveTask') || ( - isAny(element, [ - 'bpmn:StartEvent', - 'bpmn:EndEvent', - 'bpmn:IntermediateThrowEvent', - 'bpmn:BoundaryEvent', - 'bpmn:IntermediateCatchEvent' - ]) && !!getMessageEventDefinition(element) - ); + return ( + is(element, "bpmn:ReceiveTask") || + (isAny(element, [ + "bpmn:StartEvent", + "bpmn:EndEvent", + "bpmn:IntermediateThrowEvent", + "bpmn:BoundaryEvent", + "bpmn:IntermediateCatchEvent", + ]) && + !!getMessageEventDefinition(element)) + ); } export function getMessageEventDefinition(element) { - if (is(element, 'bpmn:ReceiveTask')) { - return getBusinessObject(element); - } + if (is(element, "bpmn:ReceiveTask")) { + return getBusinessObject(element); + } - return getEventDefinition(element, 'bpmn:MessageEventDefinition'); + return getEventDefinition(element, "bpmn:MessageEventDefinition"); } export function getMessage(element) { - const messageEventDefinition = getMessageEventDefinition(element); + const messageEventDefinition = getMessageEventDefinition(element); - return messageEventDefinition && messageEventDefinition.get('messageRef'); + return messageEventDefinition && messageEventDefinition.get("messageRef"); } export function getLinkEventDefinition(element) { - return getEventDefinition(element, 'bpmn:LinkEventDefinition'); + return getEventDefinition(element, "bpmn:LinkEventDefinition"); } export function getSignalEventDefinition(element) { - return getEventDefinition(element, 'bpmn:SignalEventDefinition'); + return getEventDefinition(element, "bpmn:SignalEventDefinition"); } export function isLinkSupported(element) { - return isAny(element, [ - 'bpmn:IntermediateThrowEvent', - 'bpmn:IntermediateCatchEvent' - ]) && !!getLinkEventDefinition(element); + return ( + isAny(element, [ + "bpmn:IntermediateThrowEvent", + "bpmn:IntermediateCatchEvent", + ]) && !!getLinkEventDefinition(element) + ); } export function isSignalSupported(element) { - return is(element, 'bpmn:Event') && !!getSignalEventDefinition(element); + return is(element, "bpmn:Event") && !!getSignalEventDefinition(element); } export function getSignal(element) { - const signalEventDefinition = getSignalEventDefinition(element); + const signalEventDefinition = getSignalEventDefinition(element); - return signalEventDefinition && signalEventDefinition.get('signalRef'); + return signalEventDefinition && signalEventDefinition.get("signalRef"); } export function getEscalationEventDefinition(element) { - return getEventDefinition(element, 'bpmn:EscalationEventDefinition'); + return getEventDefinition(element, "bpmn:EscalationEventDefinition"); } export function isEscalationSupported(element) { - return is(element, 'bpmn:Event') && !!getEscalationEventDefinition(element); + return is(element, "bpmn:Event") && !!getEscalationEventDefinition(element); } export function getEscalation(element) { - const escalationEventDefinition = getEscalationEventDefinition(element); + const escalationEventDefinition = getEscalationEventDefinition(element); - return escalationEventDefinition && escalationEventDefinition.get('escalationRef'); + return ( + escalationEventDefinition && escalationEventDefinition.get("escalationRef") + ); } export function isCompensationSupported(element) { - return isAny(element, [ - 'bpmn:EndEvent', - 'bpmn:IntermediateThrowEvent' - ]) && !!getCompensateEventDefinition(element); + return ( + isAny(element, ["bpmn:EndEvent", "bpmn:IntermediateThrowEvent"]) && + !!getCompensateEventDefinition(element) + ); } export function getCompensateEventDefinition(element) { - return getEventDefinition(element, 'bpmn:CompensateEventDefinition'); + return getEventDefinition(element, "bpmn:CompensateEventDefinition"); } export function getCompensateActivity(element) { - const compensateEventDefinition = getCompensateEventDefinition(element); + const compensateEventDefinition = getCompensateEventDefinition(element); - return compensateEventDefinition && compensateEventDefinition.get('activityRef'); + return ( + compensateEventDefinition && compensateEventDefinition.get("activityRef") + ); } diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ExtensionElementsUtil.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ExtensionElementsUtil.js index d39748d2..b66abacc 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ExtensionElementsUtil.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ExtensionElementsUtil.js @@ -4,11 +4,11 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import {is} from 'bpmn-js/lib/util/ModelUtil'; +import { is } from "bpmn-js/lib/util/ModelUtil"; -import {createElement} from './ElementUtil'; +import { createElement } from "./ElementUtil"; -import {isArray} from 'min-dash'; +import { isArray } from "min-dash"; /** * Get extension elements of business object. Optionally filter by type. @@ -18,33 +18,33 @@ import {isArray} from 'min-dash'; * @returns {Array} */ export function getExtensionElementsList(businessObject, type = undefined) { - const extensionElements = businessObject.get('extensionElements'); + const extensionElements = businessObject.get("extensionElements"); - if (!extensionElements) { - return []; - } + if (!extensionElements) { + return []; + } - const values = extensionElements.get('values'); + const values = extensionElements.get("values"); - if (!values || !values.length) { - return []; - } + if (!values || !values.length) { + return []; + } - if (type) { - return values.filter(value => is(value, type)); - } - return values; + if (type) { + return values.filter((value) => is(value, type)); + } + return values; } export function getExtension(element, type) { - const extensionElements = getExtensionElementsList(element); - if (!extensionElements) { - return null; - } - - return extensionElements.filter(function (e) { - return e.$instanceOf(type); - })[0]; + const extensionElements = getExtensionElementsList(element); + if (!extensionElements) { + return null; + } + + return extensionElements.filter(function (e) { + return e.$instanceOf(type); + })[0]; } /** @@ -55,49 +55,55 @@ export function getExtension(element, type) { * @param {ModdleElement|Array} extensionElementsToAdd * @param {CommandStack} commandStack */ -export function addExtensionElements(element, businessObject, extensionElementToAdd, bpmnFactory, commandStack) { - const commands = []; - - let extensionElements = businessObject.get('extensionElements'); - - // (1) create bpmn:ExtensionElements if it doesn't exist - if (!extensionElements) { - extensionElements = createElement( - 'bpmn:ExtensionElements', - { - values: [] - }, - businessObject, - bpmnFactory - ); - - commands.push({ - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: businessObject, - properties: { - extensionElements - } - } - }); - } - - extensionElementToAdd.$parent = extensionElements; - - // (2) add extension element to list +export function addExtensionElements( + element, + businessObject, + extensionElementToAdd, + bpmnFactory, + commandStack +) { + const commands = []; + + let extensionElements = businessObject.get("extensionElements"); + + // (1) create bpmn:ExtensionElements if it doesn't exist + if (!extensionElements) { + extensionElements = createElement( + "bpmn:ExtensionElements", + { + values: [], + }, + businessObject, + bpmnFactory + ); + commands.push({ - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: extensionElements, - properties: { - values: [...extensionElements.get('values'), extensionElementToAdd] - } - } + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: businessObject, + properties: { + extensionElements, + }, + }, }); - - commandStack.execute('properties-panel.multi-command-executor', commands); + } + + extensionElementToAdd.$parent = extensionElements; + + // (2) add extension element to list + commands.push({ + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: extensionElements, + properties: { + values: [...extensionElements.get("values"), extensionElementToAdd], + }, + }, + }); + + commandStack.execute("properties-panel.multi-command-executor", commands); } /** @@ -108,19 +114,26 @@ export function addExtensionElements(element, businessObject, extensionElementTo * @param {ModdleElement|Array} extensionElementsToRemove * @param {CommandStack} commandStack */ -export function removeExtensionElements(element, businessObject, extensionElementsToRemove, commandStack) { - if (!isArray(extensionElementsToRemove)) { - extensionElementsToRemove = [extensionElementsToRemove]; - } - - const extensionElements = businessObject.get('extensionElements'), - values = extensionElements.get('values').filter(value => !extensionElementsToRemove.includes(value)); - - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: extensionElements, - properties: { - values - } - }); +export function removeExtensionElements( + element, + businessObject, + extensionElementsToRemove, + commandStack +) { + if (!isArray(extensionElementsToRemove)) { + extensionElementsToRemove = [extensionElementsToRemove]; + } + + const extensionElements = businessObject.get("extensionElements"), + values = extensionElements + .get("values") + .filter((value) => !extensionElementsToRemove.includes(value)); + + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: extensionElements, + properties: { + values, + }, + }); } diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/FormTypeUtils.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/FormTypeUtils.js index 15773e02..97c1cf68 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/FormTypeUtils.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/FormTypeUtils.js @@ -4,30 +4,30 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import {isDefined} from 'min-dash'; +import { isDefined } from "min-dash"; -import { - getBusinessObject, - is -} from 'bpmn-js/lib/util/ModelUtil'; +import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil"; export function getFormRefBinding(element) { - const businessObject = getBusinessObject(element); + const businessObject = getBusinessObject(element); - return businessObject.get('camunda:formRefBinding') || 'latest'; + return businessObject.get("camunda:formRefBinding") || "latest"; } export function getFormType(element) { - const businessObject = getBusinessObject(element); + const businessObject = getBusinessObject(element); - if (isDefined(businessObject.get('camunda:formKey'))) { - return 'formKey'; - } else if (isDefined(businessObject.get('camunda:formRef'))) { - return 'formRef'; - } + if (isDefined(businessObject.get("camunda:formKey"))) { + return "formKey"; + } else if (isDefined(businessObject.get("camunda:formRef"))) { + return "formRef"; + } } export function isFormSupported(element) { - return (is(element, 'bpmn:StartEvent') && !is(element.parent, 'bpmn:SubProcess')) - || is(element, 'bpmn:UserTask'); -} \ No newline at end of file + return ( + (is(element, "bpmn:StartEvent") && + !is(element.parent, "bpmn:SubProcess")) || + is(element, "bpmn:UserTask") + ); +} diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ImplementationTypeUtils.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ImplementationTypeUtils.js index ecdd4755..7c50566a 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ImplementationTypeUtils.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ImplementationTypeUtils.js @@ -4,16 +4,10 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import { - getBusinessObject, - is -} from 'bpmn-js/lib/util/ModelUtil'; +import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil"; -import { - isAny -} from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; -import {getMessageEventDefinition} from "./EventDefinitionUtil"; -import {getExtensionElementsList} from "./ExtensionElementsUtil"; +import { isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil"; +import { getMessageEventDefinition } from "./EventDefinitionUtil"; /** * Check whether an element is camunda:ServiceTaskLike @@ -23,7 +17,7 @@ import {getExtensionElementsList} from "./ExtensionElementsUtil"; * @return {boolean} a boolean value */ export function isServiceTaskLike(element) { - return is(element, 'camunda:ServiceTaskLike'); + return is(element, "camunda:ServiceTaskLike"); } /** @@ -34,7 +28,7 @@ export function isServiceTaskLike(element) { * @return {boolean} a boolean value */ export function isDmnCapable(element) { - return is(element, 'camunda:DmnCapable'); + return is(element, "camunda:DmnCapable"); } /** @@ -45,18 +39,16 @@ export function isDmnCapable(element) { * @return {boolean} a boolean value */ export function isExternalCapable(element) { - return is(element, 'camunda:ExternalCapable'); + return is(element, "camunda:ExternalCapable"); } /** * Returns 'true' * - * @param {djs.model.Base} element - * * @return {boolean} true */ -export function isDeploymentCapable(element) { - return true; +export function isDeploymentCapable() { + return true; } /** @@ -69,20 +61,21 @@ export function isDeploymentCapable(element) { * @return {ModdleElement} the 'camunda:ServiceTaskLike' business object */ export function getServiceTaskLikeBusinessObject(element) { - - if (is(element, 'bpmn:IntermediateThrowEvent') || is(element, 'bpmn:EndEvent')) { - - // change business object to 'messageEventDefinition' when - // the element is a message intermediate throw event or message end event - // because the camunda extensions (e.g. camunda:class) are in the message - // event definition tag and not in the intermediate throw event or end event tag - const messageEventDefinition = getMessageEventDefinition(element); - if (messageEventDefinition) { - element = messageEventDefinition; - } + if ( + is(element, "bpmn:IntermediateThrowEvent") || + is(element, "bpmn:EndEvent") + ) { + // change business object to 'messageEventDefinition' when + // the element is a message intermediate throw event or message end event + // because the camunda extensions (e.g. camunda:class) are in the message + // event definition tag and not in the intermediate throw event or end event tag + const messageEventDefinition = getMessageEventDefinition(element); + if (messageEventDefinition) { + element = messageEventDefinition; } + } - return isServiceTaskLike(element) && getBusinessObject(element); + return isServiceTaskLike(element) && getBusinessObject(element); } // /** @@ -156,11 +149,13 @@ export function getServiceTaskLikeBusinessObject(element) { // } export function isListener(element) { - return this.isTaskListener(element) || this.isExecutionListener(element); + return this.isTaskListener(element) || this.isExecutionListener(element); } export function getListenerBusinessObject(businessObject) { - if (isAny(businessObject, ['camunda:ExecutionListener', 'camunda:TaskListener'])) { - return businessObject; - } + if ( + isAny(businessObject, ["camunda:ExecutionListener", "camunda:TaskListener"]) + ) { + return businessObject; + } } diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/InputOutputUtil.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/InputOutputUtil.js index 7cf6645e..c30c287a 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/InputOutputUtil.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/InputOutputUtil.js @@ -4,25 +4,20 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import { - isAny -} from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; +import { isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil"; -import { - getBusinessObject, - is -} from 'bpmn-js/lib/util/ModelUtil'; -import {getExtensionElementsList} from "./ExtensionElementsUtil"; -import {createElement, nextId} from "./ElementUtil"; +import { getBusinessObject, is } from "bpmn-js/lib/util/ModelUtil"; +import { getExtensionElementsList } from "./ExtensionElementsUtil"; +import { createElement, nextId } from "./ElementUtil"; function getElements(businessObject, type, property) { - const elements = getExtensionElementsList(businessObject, type); - return !property ? elements : (elements[0] || {})[property] || []; + const elements = getExtensionElementsList(businessObject, type); + return !property ? elements : (elements[0] || {})[property] || []; } function getParameters(element, prop) { - const inputOutput = getInputOutput(element); - return (inputOutput && inputOutput.get(prop)) || []; + const inputOutput = getInputOutput(element); + return (inputOutput && inputOutput.get(prop)) || []; } /** @@ -33,16 +28,15 @@ function getParameters(element, prop) { * @return {ModdleElement} the inputOutput object */ export function getInputOutput(element) { - if (is(element, 'camunda:Connector')) { - return element.get('inputOutput'); - } + if (is(element, "camunda:Connector")) { + return element.get("inputOutput"); + } - const businessObject = getBusinessObject(element); + const businessObject = getBusinessObject(element); - return (getElements(businessObject, 'camunda:InputOutput') || [])[0]; + return (getElements(businessObject, "camunda:InputOutput") || [])[0]; } - /** * Return all input parameters existing in the business object, and * an empty array if none exist. @@ -52,7 +46,7 @@ export function getInputOutput(element) { * @return {Array} a list of input parameter objects */ export function getInputParameters(element) { - return getParameters(element, 'inputParameters'); + return getParameters(element, "inputParameters"); } /** @@ -64,122 +58,137 @@ export function getInputParameters(element) { * @return {Array} a list of output parameter objects */ export function getOutputParameters(element) { - return getParameters(element, 'outputParameters'); + return getParameters(element, "outputParameters"); } - export function isInputOutputSupported(element) { - const businessObject = getBusinessObject(element); - - return ( - is(businessObject, 'bpmn:FlowNode') && !( - isAny(businessObject, ['bpmn:StartEvent', 'bpmn:BoundaryEvent', 'bpmn:Gateway']) || - is(businessObject, 'bpmn:SubProcess') && businessObject.get('triggeredByEvent') - ) - ); + const businessObject = getBusinessObject(element); + + return ( + is(businessObject, "bpmn:FlowNode") && + !( + isAny(businessObject, [ + "bpmn:StartEvent", + "bpmn:BoundaryEvent", + "bpmn:Gateway", + ]) || + (is(businessObject, "bpmn:SubProcess") && + businessObject.get("triggeredByEvent")) + ) + ); } export function areInputParametersSupported(element) { - return isInputOutputSupported(element); + return isInputOutputSupported(element); } export function areOutputParametersSupported(element) { - const businessObject = getBusinessObject(element); - return ( - isInputOutputSupported(element) && - !is(businessObject, 'bpmn:EndEvent') && - !businessObject.loopCharacteristics - ); + const businessObject = getBusinessObject(element); + return ( + isInputOutputSupported(element) && + !is(businessObject, "bpmn:EndEvent") && + !businessObject.loopCharacteristics + ); } export function getInputOutputType(parameter) { - const definitionTypes = { - 'camunda:Map': 'map', - 'camunda:List': 'list', - 'camunda:Script': 'script' - }; + const definitionTypes = { + "camunda:Map": "map", + "camunda:List": "list", + "camunda:Script": "script", + }; - let type = 'stringOrExpression'; + let type = "stringOrExpression"; - const definition = parameter.get('definition'); - if (typeof definition !== 'undefined') { - type = definitionTypes[definition.$type]; - } + const definition = parameter.get("definition"); + if (typeof definition !== "undefined") { + type = definitionTypes[definition.$type]; + } - return type; + return type; } export function CreateParameterCmd(element, type, parent, bpmnFactory) { - const isInput = type === 'camunda:InputParameter'; - - const newParameter = createElement(type, { - name: nextId(isInput ? 'Input_' : 'Output_') - }, parent, bpmnFactory); - - const propertyName = isInput ? 'inputParameters' : 'outputParameters'; - - return { - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: parent, - properties: { - [propertyName]: [...parent.get(propertyName), newParameter] - } - } - }; + const isInput = type === "camunda:InputParameter"; + + const newParameter = createElement( + type, + { + name: nextId(isInput ? "Input_" : "Output_"), + }, + parent, + bpmnFactory + ); + + const propertyName = isInput ? "inputParameters" : "outputParameters"; + + return { + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: parent, + properties: { + [propertyName]: [...parent.get(propertyName), newParameter], + }, + }, + }; } export function AddParameterCmd(element, type, bpmnFactory) { - const commands = []; - const businessObject = getBusinessObject(element); - - let extensionElements = businessObject.get('extensionElements'); - - // (1) ensure extension elements - if (!extensionElements) { - extensionElements = createElement( - 'bpmn:ExtensionElements', - {values: []}, - businessObject, - bpmnFactory - ); - - commands.push({ - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: businessObject, - properties: {extensionElements} - } - }); - } - - // (2) ensure inputOutput - let inputOutput = getInputOutput(element); - - if (!inputOutput) { - const parent = extensionElements; - - inputOutput = createElement('camunda:InputOutput', { - inputParameters: [], - outputParameters: [] - }, parent, bpmnFactory); - - commands.push({ - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: extensionElements, - properties: { - values: [...extensionElements.get('values'), inputOutput] - } - } - }); - } - - // (3) create + add parameter - commands.push(CreateParameterCmd(element, type, inputOutput, bpmnFactory)); - - return commands; -} \ No newline at end of file + const commands = []; + const businessObject = getBusinessObject(element); + + let extensionElements = businessObject.get("extensionElements"); + + // (1) ensure extension elements + if (!extensionElements) { + extensionElements = createElement( + "bpmn:ExtensionElements", + { values: [] }, + businessObject, + bpmnFactory + ); + + commands.push({ + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: businessObject, + properties: { extensionElements }, + }, + }); + } + + // (2) ensure inputOutput + let inputOutput = getInputOutput(element); + + if (!inputOutput) { + const parent = extensionElements; + + inputOutput = createElement( + "camunda:InputOutput", + { + inputParameters: [], + outputParameters: [], + }, + parent, + bpmnFactory + ); + + commands.push({ + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: extensionElements, + properties: { + values: [...extensionElements.get("values"), inputOutput], + }, + }, + }); + } + + // (3) create + add parameter + commands.push(CreateParameterCmd(element, type, inputOutput, bpmnFactory)); + + return commands; +} diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ValidationUtil.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ValidationUtil.js index 38375c25..a2d1b959 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/ValidationUtil.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/ValidationUtil.js @@ -22,36 +22,34 @@ const ID_REGEX = /^[a-z_][\w-.]*$/i; * @return {String} error message */ export function isIdValid(element, idValue, translate) { - const assigned = element.$model.ids.assigned(idValue); - const idAlreadyExists = assigned && assigned !== element; + const assigned = element.$model.ids.assigned(idValue); + const idAlreadyExists = assigned && assigned !== element; - if (!idValue) { - return translate('ID must not be empty.'); - } + if (!idValue) { + return translate("ID must not be empty."); + } - if (idAlreadyExists) { - return translate('ID must be unique.'); - } + if (idAlreadyExists) { + return translate("ID must be unique."); + } - return validateId(idValue, translate); + return validateId(idValue, translate); } export function validateId(idValue, translate) { + if (containsSpace(idValue)) { + return translate("ID must not contain spaces."); + } - if (containsSpace(idValue)) { - return translate('ID must not contain spaces.'); + if (!ID_REGEX.test(idValue)) { + if (QNAME_REGEX.test(idValue)) { + return translate("ID must not contain prefix."); } - if (!ID_REGEX.test(idValue)) { - - if (QNAME_REGEX.test(idValue)) { - return translate('ID must not contain prefix.'); - } - - return translate('ID must be a valid QName.'); - } + return translate("ID must be a valid QName."); + } } export function containsSpace(value) { - return SPACE_REGEX.test(value); -} \ No newline at end of file + return SPACE_REGEX.test(value); +} diff --git a/components/bpmn-q/modeler-component/editor/util/camunda-utils/generateImage.js b/components/bpmn-q/modeler-component/editor/util/camunda-utils/generateImage.js index a7ae2aa5..9b45f361 100644 --- a/components/bpmn-q/modeler-component/editor/util/camunda-utils/generateImage.js +++ b/components/bpmn-q/modeler-component/editor/util/camunda-utils/generateImage.js @@ -4,56 +4,59 @@ * This code and the accompanying materials are made available by camunda under the * terms of the MIT License. */ -import canvg from 'canvg-browser'; +import canvg from "canvg-browser"; // list of defined encodings -const ENCODINGS = [ - 'image/png', - 'image/jpeg' -]; +const ENCODINGS = ["image/png", "image/jpeg"]; const INITIAL_SCALE = 3; const FINAL_SCALE = 1; const SCALE_STEP = 1; -const DATA_URL_REGEX = /^data:((?:\w+\/(?:(?!;).)+)?)((?:;[\w\W]*?[^;])*),(.+)$/; - +const DATA_URL_REGEX = + /^data:((?:\w+\/(?:(?!;).)+)?)((?:;[\w\W]*?[^;])*),(.+)$/; export default function generateImage(type, svg) { - const encoding = 'image/' + type; - - if (ENCODINGS.indexOf(encoding) === -1) { - throw new Error('<' + type + '> is an unknown type for converting svg to image'); - } + const encoding = "image/" + type; - const initialSVG = svg; + if (ENCODINGS.indexOf(encoding) === -1) { + throw new Error( + "<" + type + "> is an unknown type for converting svg to image" + ); + } - let dataURL = ''; + const initialSVG = svg; - for (let scale = INITIAL_SCALE; scale >= FINAL_SCALE; scale -= SCALE_STEP) { + let dataURL = ""; - let canvas = document.createElement('canvas'); + for (let scale = INITIAL_SCALE; scale >= FINAL_SCALE; scale -= SCALE_STEP) { + let canvas = document.createElement("canvas"); - svg = initialSVG.replace(/width="([^"]+)" height="([^"]+)"/, function (_, widthStr, heightStr) { - return `width="${parseInt(widthStr, 10) * scale}" height="${parseInt(heightStr, 10) * scale}"`; - }); + svg = initialSVG.replace( + /width="([^"]+)" height="([^"]+)"/, + function (_, widthStr, heightStr) { + return `width="${parseInt(widthStr, 10) * scale}" height="${ + parseInt(heightStr, 10) * scale + }"`; + } + ); - canvg(canvas, svg); + canvg(canvas, svg); - // make the background white for every format - let context = canvas.getContext('2d'); + // make the background white for every format + let context = canvas.getContext("2d"); - context.globalCompositeOperation = 'destination-over'; - context.fillStyle = 'white'; + context.globalCompositeOperation = "destination-over"; + context.fillStyle = "white"; - context.fillRect(0, 0, canvas.width, canvas.height); + context.fillRect(0, 0, canvas.width, canvas.height); - dataURL = canvas.toDataURL(encoding); + dataURL = canvas.toDataURL(encoding); - if (DATA_URL_REGEX.test(dataURL)) { - return dataURL; - } + if (DATA_URL_REGEX.test(dataURL)) { + return dataURL; } + } - throw new Error('Error happened generating image. Diagram size is too big.'); + throw new Error("Error happened generating image. Diagram size is too big."); } diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/Constants.js b/components/bpmn-q/modeler-component/extensions/data-extension/Constants.js index ba0289c1..707137f7 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/Constants.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/Constants.js @@ -1,19 +1,21 @@ // Type names of the data flow extension elements -export const DATA_MAP_OBJECT = 'dataflow:DataMapObject'; -export const DATA_STORE_MAP = 'dataflow:DataStoreMap'; -export const TRANSFORMATION_TASK = 'dataflow:TransformationTask'; -export const TRANSFORMATION_ASSOCIATION = 'dataflow:TransformationAssociation'; -export const INPUT_TRANSFORMATION_ASSOCIATION = 'dataflow:InputTransformationAssociation'; -export const OUTPUT_TRANSFORMATION_ASSOCIATION = 'dataflow:OutputTransformationAssociation'; -export const KEY_VALUE_ENTRY = 'dataflow:KeyValueEntry'; +export const DATA_MAP_OBJECT = "dataflow:DataMapObject"; +export const DATA_STORE_MAP = "dataflow:DataStoreMap"; +export const TRANSFORMATION_TASK = "dataflow:TransformationTask"; +export const TRANSFORMATION_ASSOCIATION = "dataflow:TransformationAssociation"; +export const INPUT_TRANSFORMATION_ASSOCIATION = + "dataflow:InputTransformationAssociation"; +export const OUTPUT_TRANSFORMATION_ASSOCIATION = + "dataflow:OutputTransformationAssociation"; +export const KEY_VALUE_ENTRY = "dataflow:KeyValueEntry"; // Property names of the data flow extension elements -export const CONTENT = 'content'; -export const DETAILS = 'details'; -export const PARAMETERS = 'parameters'; -export const EXPRESSIONS = 'expressions'; +export const CONTENT = "content"; +export const DETAILS = "details"; +export const PARAMETERS = "parameters"; +export const EXPRESSIONS = "expressions"; // Unique identifiers of the icon SVGs -export const TASK_TYPE_TRANSFORMATION_TASK = 'TASK_TYPE_TRANSFORMATION_TASK'; -export const DATA_TYPE_DATA_MAP_OBJECT = 'DATA_TYPE_DATA_MAP_OBJECT'; -export const DATA_TYPE_DATA_STORE_MAP = 'DATA_TYPE_DATA_STORE_MAP'; \ No newline at end of file +export const TASK_TYPE_TRANSFORMATION_TASK = "TASK_TYPE_TRANSFORMATION_TASK"; +export const DATA_TYPE_DATA_MAP_OBJECT = "DATA_TYPE_DATA_MAP_OBJECT"; +export const DATA_TYPE_DATA_STORE_MAP = "DATA_TYPE_DATA_STORE_MAP"; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/DataFlowPlugin.js b/components/bpmn-q/modeler-component/extensions/data-extension/DataFlowPlugin.js index d4a31260..af7e64ab 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/DataFlowPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/DataFlowPlugin.js @@ -1,40 +1,44 @@ -import React from 'react'; +import React from "react"; -import DataFlowExtensionModule from './'; -import TransformationButton from '../../editor/ui/TransformationButton'; -import {getModeler} from '../../editor/ModelerHandler'; -import {getXml} from '../../editor/util/IoUtilities'; -import {startDataFlowReplacementProcess} from './transformation/TransformationManager'; -import TransformationTaskConfigurationsTab from './transf-task-configs/TransformationTaskConfigurationsTab'; -import dataStyles from './resources/data-flow-styles.css'; +import DataFlowExtensionModule from "./"; +import TransformationButton from "../../editor/ui/TransformationButton"; +import { startDataFlowReplacementProcess } from "./transformation/TransformationManager"; +import TransformationTaskConfigurationsTab from "./transf-task-configs/TransformationTaskConfigurationsTab"; +import dataStyles from "./resources/data-flow-styles.css"; import ExtensibleButton from "../../editor/ui/ExtensibleButton"; import UpdateTransformationTaskConfigurationsButton from "./ui/UpdateTransformationConfigurations"; -let dataflowModdleDescriptor = require('./resources/data-flow-extension.json'); +let dataflowModdleDescriptor = require("./resources/data-flow-extension.json"); /** * Plugin Object of the DataFlow extension. Used to register the plugin in the plugin handler of the modeler. */ export default { - name: 'dataflow', - buttons: []} - title="DataFlow" - styleClass="dataflow-plugin-icon" - description="Show buttons of the QHAna plugin"/> - ], - configTabs: [ - { - tabId: 'DataEndpointsTab', - tabTitle: 'Data Flow Plugin', - configTab: TransformationTaskConfigurationsTab, - }, - ], - extensionModule: DataFlowExtensionModule, - moddleDescription: dataflowModdleDescriptor, - styling: [dataStyles], - transformExtensionButton: { - return await startDataFlowReplacementProcess(xml); - } - }/>, -}; \ No newline at end of file + name: "dataflow", + buttons: [ + ]} + title="DataFlow" + styleClass="dataflow-plugin-icon" + description="Show buttons of the QHAna plugin" + />, + ], + configTabs: [ + { + tabId: "DataEndpointsTab", + tabTitle: "Data Flow Plugin", + configTab: TransformationTaskConfigurationsTab, + }, + ], + extensionModule: DataFlowExtensionModule, + moddleDescription: dataflowModdleDescriptor, + styling: [dataStyles], + transformExtensionButton: ( + { + return await startDataFlowReplacementProcess(xml); + }} + /> + ), +}; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/config/DataConfigManager.js b/components/bpmn-q/modeler-component/extensions/data-extension/config/DataConfigManager.js index d42aa0f6..72d296b7 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/config/DataConfigManager.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/config/DataConfigManager.js @@ -1,8 +1,8 @@ -import {getPluginConfig} from "../../../editor/plugin/PluginConfigHandler"; +import { getPluginConfig } from "../../../editor/plugin/PluginConfigHandler"; // default config entries used if no value is specified in the initial plugin config const defaultConfig = { - configurationsEndpoint: process.env.SERVICE_DATA_CONFIG + configurationsEndpoint: process.env.SERVICE_DATA_CONFIG, }; // current config @@ -14,10 +14,13 @@ let config = {}; * @return {string} the currently specified endpoint url of the Configurations endpoint */ export function getConfigurationsEndpoint() { - if (config.configurationsEndpoint === undefined) { - setConfigurationsEndpoint(getPluginConfig('dataflow').configurationsEndpoint || defaultConfig.configurationsEndpoint); - } - return config.configurationsEndpoint; + if (config.configurationsEndpoint === undefined) { + setConfigurationsEndpoint( + getPluginConfig("dataflow").configurationsEndpoint || + defaultConfig.configurationsEndpoint + ); + } + return config.configurationsEndpoint; } /** @@ -26,16 +29,15 @@ export function getConfigurationsEndpoint() { * @param configurationsEndpoint the endpoint url of the transformation task Configurations endpoint */ export function setConfigurationsEndpoint(configurationsEndpoint) { - if (configurationsEndpoint !== null && configurationsEndpoint !== undefined) { - - // remove trailing slashes - config.configurationsEndpoint = configurationsEndpoint.replace(/\/$/, ''); - } + if (configurationsEndpoint !== null && configurationsEndpoint !== undefined) { + // remove trailing slashes + config.configurationsEndpoint = configurationsEndpoint.replace(/\/$/, ""); + } } /** * Resets the all config entries */ export function resetConfig() { - config = {}; -} \ No newline at end of file + config = {}; +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/index.js b/components/bpmn-q/modeler-component/extensions/data-extension/index.js index 50f5a2f4..61d2e849 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/index.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/index.js @@ -1,23 +1,23 @@ -import DataFlowRenderer from './rendering/DataFlowRenderer'; -import DataFlowReplaceMenuProvider from './menu/DataFlowReplaceMenuProvider'; -import DataFlowPaletteProvider from './palette/DataFlowPaletteProvider'; -import DataFlowRulesProvider from './rules/DataFlowRulesProvider'; -import DataReplaceConnectionBehavior from './rules/DataReplaceConnectionBehaviour'; -import DataFlowPropertiesProvider from './properties-panel/DataFlowPropertiesProvider'; +import DataFlowRenderer from "./rendering/DataFlowRenderer"; +import DataFlowReplaceMenuProvider from "./menu/DataFlowReplaceMenuProvider"; +import DataFlowPaletteProvider from "./palette/DataFlowPaletteProvider"; +import DataFlowRulesProvider from "./rules/DataFlowRulesProvider"; +import DataReplaceConnectionBehavior from "./rules/DataReplaceConnectionBehaviour"; +import DataFlowPropertiesProvider from "./properties-panel/DataFlowPropertiesProvider"; export default { - __init__: [ - 'dataFlowRenderer', - 'dataFlowMenuProvider', - 'dataFlowPaletteProvider', - 'dataFlowRules', - 'replaceConnectionBehavior', - 'dataFlowPropertiesProvider' - ], - dataFlowRenderer: ['type', DataFlowRenderer], - dataFlowMenuProvider: ['type', DataFlowReplaceMenuProvider], - dataFlowPaletteProvider: ['type', DataFlowPaletteProvider], - dataFlowRules: ['type', DataFlowRulesProvider], - replaceConnectionBehavior: ['type', DataReplaceConnectionBehavior], - dataFlowPropertiesProvider: ['type', DataFlowPropertiesProvider] -}; \ No newline at end of file + __init__: [ + "dataFlowRenderer", + "dataFlowMenuProvider", + "dataFlowPaletteProvider", + "dataFlowRules", + "replaceConnectionBehavior", + "dataFlowPropertiesProvider", + ], + dataFlowRenderer: ["type", DataFlowRenderer], + dataFlowMenuProvider: ["type", DataFlowReplaceMenuProvider], + dataFlowPaletteProvider: ["type", DataFlowPaletteProvider], + dataFlowRules: ["type", DataFlowRulesProvider], + replaceConnectionBehavior: ["type", DataReplaceConnectionBehavior], + dataFlowPropertiesProvider: ["type", DataFlowPropertiesProvider], +}; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceMenuProvider.js b/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceMenuProvider.js index 2451670a..84734d4d 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceMenuProvider.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceMenuProvider.js @@ -1,259 +1,315 @@ -import {is} from 'bpmn-js/lib/util/ModelUtil'; -import * as replaceOptions from './DataFlowReplaceOptions'; +import { is } from "bpmn-js/lib/util/ModelUtil"; +import * as replaceOptions from "./DataFlowReplaceOptions"; import { - createMenuEntries, - createMenuEntry, - createMoreOptionsEntryWithReturn + createMenuEntries, + createMenuEntry, + createMoreOptionsEntryWithReturn, } from "../../../editor/util/PopupMenuUtilities"; -import * as consts from '../Constants'; -import {createConfigurationsEntries} from '../../../editor/configurations/ConfigurationsUtil'; -import {getTransformationTaskConfigurations} from '../transf-task-configs/TransformationTaskConfigurations'; -import {replaceConnection} from '../../../editor/util/ModellingUtilities'; -import { filter } from 'min-dash'; -import { isDifferentType } from 'bpmn-js/lib/features/popup-menu/util/TypeUtil'; +import * as consts from "../Constants"; +import { createConfigurationsEntries } from "../../../editor/configurations/ConfigurationsUtil"; +import { getTransformationTaskConfigurations } from "../transf-task-configs/TransformationTaskConfigurations"; +import { replaceConnection } from "../../../editor/util/ModellingUtilities"; +import { filter } from "min-dash"; +import { isDifferentType } from "bpmn-js/lib/features/popup-menu/util/TypeUtil"; /** * Menu Provider for bpmn-replace which is opened for a diagram element. Adds replacement entries to replace the element with the * data flow extension elements. */ export default class DataFlowReplaceMenuProvider { - constructor(popupMenu, translate, bpmnReplace, modeling, bpmnFactory, moddle, elementRegistry, commandStack) { - popupMenu.registerProvider("bpmn-replace", this); + constructor( + popupMenu, + translate, + bpmnReplace, + modeling, + bpmnFactory, + moddle, + elementRegistry, + commandStack + ) { + popupMenu.registerProvider("bpmn-replace", this); - this.replaceElement = bpmnReplace.replaceElement; - this.translate = translate; - this.modeling = modeling; - this.bpmnFactory = bpmnFactory; - this.moddle = moddle; - this.elementRegistry = elementRegistry; - this.commandStack = commandStack; - this.popupMenu = popupMenu; - } - - /** - * Define header entries for the data flow elements - * - * @param element - * @returns {(function(*): ({}))|*} - */ - getPopupMenuHeaderEntries(element) { - return function (entries) { - - // remove all header entries (it is only the collection marker) for DataMapObjects because they do not support them - if (is(element, consts.DATA_MAP_OBJECT)) { - return {}; - } - return entries; - }; - } - - /** - * Overwrites the default menu provider to add menu entries to replace the element with DataFlow extension elements - * - * @param element the element for which the replacement entries are requested - * @returns {*} an array with menu entries - */ - getPopupMenuEntries(element) { - const self = this; - return function (entries) { - - // do not show entries for extension elements of other plugins - if (!(element.type.startsWith('bpmn') || element.type.startsWith('dataflow'))) { - return entries; - } + this.replaceElement = bpmnReplace.replaceElement; + this.translate = translate; + this.modeling = modeling; + this.bpmnFactory = bpmnFactory; + this.moddle = moddle; + this.elementRegistry = elementRegistry; + this.commandStack = commandStack; + this.popupMenu = popupMenu; + } - // set entries for the transformation task configurations as replacement for a DataFlow transformation task - if (is(element, consts.TRANSFORMATION_TASK)) { - let configEntries = {}; - const dataConfigurations = createConfigurationsEntries( - element, - 'dataflow-transformation-task-icon', - getTransformationTaskConfigurations(), - self.bpmnFactory, - self.modeling, - self.commandStack, - self.replaceElement - ); + /** + * Define header entries for the data flow elements + * + * @param element + * @returns {(function(*): ({}))|*} + */ + getPopupMenuHeaderEntries(element) { + return function (entries) { + // remove all header entries (it is only the collection marker) for DataMapObjects because they do not support them + if (is(element, consts.DATA_MAP_OBJECT)) { + return {}; + } + return entries; + }; + } - if (element.businessObject.name) { - configEntries = createMenuEntries(element, replaceOptions.TASK, self.translate, self.replaceElement); - return Object.assign(configEntries, dataConfigurations); - } - return Object.assign(dataConfigurations, entries); - } + /** + * Overwrites the default menu provider to add menu entries to replace the element with DataFlow extension elements + * + * @param element the element for which the replacement entries are requested + * @returns {*} an array with menu entries + */ + getPopupMenuEntries(element) { + const self = this; + return function (entries) { + // do not show entries for extension elements of other plugins + if ( + !( + element.type.startsWith("bpmn") || element.type.startsWith("dataflow") + ) + ) { + return entries; + } - // add MoreOptionsEntry for transformation task as replacement for BPMN task types - if (is(element, 'bpmn:Task')) { - const taskEntries = self.createTransformationTasksEntries(element); - return Object.assign(taskEntries, entries); - } - - // add entries for data map objects as replacement for BPMN data objects - if (is(element, 'bpmn:DataObjectReference')) { - let filteredOptions = filter(replaceOptions.DATA_OBJECT, isDifferentType(element)); - const dataEntries = createMenuEntries(element, filteredOptions, self.translate, self.replaceElement); - return Object.assign(dataEntries, entries); - } - - // add entries for data store maps as replacement for BPMN data stores - if (is(element, 'bpmn:DataStoreReference')) { - let filteredOptions = filter(replaceOptions.DATA_STORE, isDifferentType(element)); - const storeEntries = createMenuEntries(element, filteredOptions, self.translate, self.replaceElement); - return Object.assign(storeEntries, entries); - } + // set entries for the transformation task configurations as replacement for a DataFlow transformation task + if (is(element, consts.TRANSFORMATION_TASK)) { + let configEntries = {}; + const dataConfigurations = createConfigurationsEntries( + element, + "dataflow-transformation-task-icon", + getTransformationTaskConfigurations(), + self.bpmnFactory, + self.modeling, + self.commandStack, + self.replaceElement + ); - // set entry for transformation association as replacement for BPMN data association - if (is(element, 'bpmn:DataAssociation') && !is(element, consts.TRANSFORMATION_ASSOCIATION)) { - const associationEntry = self.createTransformationAssociationEntry(element); - if (associationEntry) { - return associationEntry; - } - } + if (element.businessObject.name) { + configEntries = createMenuEntries( + element, + replaceOptions.TASK, + self.translate, + self.replaceElement + ); + return Object.assign(configEntries, dataConfigurations); + } + return Object.assign(dataConfigurations, entries); + } - // add entry for data association as replacement for DataFlow transformation association - if (is(element, consts.TRANSFORMATION_ASSOCIATION)) { - const dataAssociationEntry = self.createDataAssociationEntry(element); - if (dataAssociationEntry) { - return Object.assign(dataAssociationEntry, entries); - } - } - return entries; - }; - } + // add MoreOptionsEntry for transformation task as replacement for BPMN task types + if (is(element, "bpmn:Task")) { + const taskEntries = self.createTransformationTasksEntries(element); + return Object.assign(taskEntries, entries); + } - /** - * Create MoreOptionEntries with replacement entries for a transformation task, including the loaded configurations - * for transformation tasks. - * - * @param element The element the replacement entries are requested for - * @returns {{'replace-by-more-transf-task-options': {label: string, className: string, action: Function}}} - */ - createTransformationTasksEntries(element) { - const popupMenu = this.popupMenu; - const translate = this.translate; - const replaceElement = this.replaceElement; - const bpmnFactory = this.bpmnFactory; - const modeling = this.modeling; - const commandStack = this.commandStack; + // add entries for data map objects as replacement for BPMN data objects + if (is(element, "bpmn:DataObjectReference")) { + let filteredOptions = filter( + replaceOptions.DATA_OBJECT, + isDifferentType(element) + ); + const dataEntries = createMenuEntries( + element, + filteredOptions, + self.translate, + self.replaceElement + ); + return Object.assign(dataEntries, entries); + } - // create replacement entries for each loaded transformation task configuration - let options = createConfigurationsEntries( - element, - 'bpmn-icon-dataflow-transformation-task', - getTransformationTaskConfigurations(), - bpmnFactory, - modeling, - commandStack, - replaceElement + // add entries for data store maps as replacement for BPMN data stores + if (is(element, "bpmn:DataStoreReference")) { + let filteredOptions = filter( + replaceOptions.DATA_STORE, + isDifferentType(element) ); - let filteredOptions = filter(replaceOptions.TASK, isDifferentType(element)); - options = Object.assign(createMenuEntries(element, filteredOptions, translate, replaceElement), options); + const storeEntries = createMenuEntries( + element, + filteredOptions, + self.translate, + self.replaceElement + ); + return Object.assign(storeEntries, entries); + } - return { - ['replace-by-more-transf-task-options']: createMoreOptionsEntryWithReturn( - element, - 'Transformation Tasks', - 'Transformation Tasks', - popupMenu, - options, - 'dataflow-transformation-tasks-menu-icon' - ) - }; - } + // set entry for transformation association as replacement for BPMN data association + if ( + is(element, "bpmn:DataAssociation") && + !is(element, consts.TRANSFORMATION_ASSOCIATION) + ) { + const associationEntry = + self.createTransformationAssociationEntry(element); + if (associationEntry) { + return associationEntry; + } + } - /** - * Create replacement entry to replace a data association with a DataFlow transformation association if the data - * association connects a DataMapObject with an activity. - * - * @param element The element the replacement entries are requested for - * @returns {{}} The created replacement entry - */ - createTransformationAssociationEntry(element) { + // add entry for data association as replacement for DataFlow transformation association + if (is(element, consts.TRANSFORMATION_ASSOCIATION)) { + const dataAssociationEntry = self.createDataAssociationEntry(element); + if (dataAssociationEntry) { + return Object.assign(dataAssociationEntry, entries); + } + } + return entries; + }; + } - const modeling = this.modeling; - const translate = this.translate; - const replaceElement = this.replaceElement; + /** + * Create MoreOptionEntries with replacement entries for a transformation task, including the loaded configurations + * for transformation tasks. + * + * @param element The element the replacement entries are requested for + * @returns {{'replace-by-more-transf-task-options': {label: string, className: string, action: Function}}} + */ + createTransformationTasksEntries(element) { + const popupMenu = this.popupMenu; + const translate = this.translate; + const replaceElement = this.replaceElement; + const bpmnFactory = this.bpmnFactory; + const modeling = this.modeling; + const commandStack = this.commandStack; - const entryId = 'replace-with-transformation-flow'; + // create replacement entries for each loaded transformation task configuration + let options = createConfigurationsEntries( + element, + "bpmn-icon-dataflow-transformation-task", + getTransformationTaskConfigurations(), + bpmnFactory, + modeling, + commandStack, + replaceElement + ); + let filteredOptions = filter(replaceOptions.TASK, isDifferentType(element)); + options = Object.assign( + createMenuEntries(element, filteredOptions, translate, replaceElement), + options + ); - // if DataObjectMap -- TransformationAssociation -> Activity - if (is(element.source, consts.DATA_MAP_OBJECT) && - (is(element.target, 'bpmn:Activity') && !is(element.target, consts.DATA_MAP_OBJECT))) { + return { + ["replace-by-more-transf-task-options"]: createMoreOptionsEntryWithReturn( + element, + "Transformation Tasks", + "Transformation Tasks", + popupMenu, + options, + "dataflow-transformation-tasks-menu-icon" + ), + }; + } - // create definition for menu entry to replace with a transformation association - const definition = { - label: 'Transformation Association', - id: entryId, - className: 'bpmn-icon-dataflow-transformation-association', - }; + /** + * Create replacement entry to replace a data association with a DataFlow transformation association if the data + * association connects a DataMapObject with an activity. + * + * @param element The element the replacement entries are requested for + * @returns {{}} The created replacement entry + */ + createTransformationAssociationEntry(element) { + const modeling = this.modeling; + const translate = this.translate; + const replaceElement = this.replaceElement; - // define action to replace with a transformation association - const action = function () { - let associationType = consts.OUTPUT_TRANSFORMATION_ASSOCIATION; + const entryId = "replace-with-transformation-flow"; - // replace with an input or output transformation association depending on the type of the data association - if (is(element, 'bpmn:DataInputAssociation')) { - associationType = consts.INPUT_TRANSFORMATION_ASSOCIATION; - } - replaceConnection(element, associationType, modeling); - }; + // if DataObjectMap -- TransformationAssociation -> Activity + if ( + is(element.source, consts.DATA_MAP_OBJECT) && + is(element.target, "bpmn:Activity") && + !is(element.target, consts.DATA_MAP_OBJECT) + ) { + // create definition for menu entry to replace with a transformation association + const definition = { + label: "Transformation Association", + id: entryId, + className: "bpmn-icon-dataflow-transformation-association", + }; - // create menu entry - return { - [entryId]: createMenuEntry(element, definition, translate, replaceElement, action), - }; - } - } + // define action to replace with a transformation association + const action = function () { + let associationType = consts.OUTPUT_TRANSFORMATION_ASSOCIATION; - /** - * Create replacement entry to replace a DataFlow transformation association with a data association if the - * transformation association does NOT connect two DataMapObjects. - * - * @param element The element the replacement entries are requested for - * @returns {{}} The created replacement entry - */ - createDataAssociationEntry(element) { - const modeling = this.modeling; - const translate = this.translate; - const replaceElement = this.replaceElement; + // replace with an input or output transformation association depending on the type of the data association + if (is(element, "bpmn:DataInputAssociation")) { + associationType = consts.INPUT_TRANSFORMATION_ASSOCIATION; + } + replaceConnection(element, associationType, modeling); + }; - // create entry if transformation association does NOT connect two data map objects - if (!(is(element.source, consts.DATA_MAP_OBJECT) && is(element.target, consts.DATA_MAP_OBJECT))) { + // create menu entry + return { + [entryId]: createMenuEntry( + element, + definition, + translate, + replaceElement, + action + ), + }; + } + } - // create definition of menu entry - const entryId = 'replace-with-data-association'; - const definition = { - label: 'Data Association', - id: entryId, - className: 'bpmn-icon-dataflow-data-association-icon', - }; + /** + * Create replacement entry to replace a DataFlow transformation association with a data association if the + * transformation association does NOT connect two DataMapObjects. + * + * @param element The element the replacement entries are requested for + * @returns {{}} The created replacement entry + */ + createDataAssociationEntry(element) { + const modeling = this.modeling; + const translate = this.translate; + const replaceElement = this.replaceElement; - // create action to replace the transformation association by a data association - const action = function () { - let associationType = 'bpmn:DataOutputAssociation'; + // create entry if transformation association does NOT connect two data map objects + if ( + !( + is(element.source, consts.DATA_MAP_OBJECT) && + is(element.target, consts.DATA_MAP_OBJECT) + ) + ) { + // create definition of menu entry + const entryId = "replace-with-data-association"; + const definition = { + label: "Data Association", + id: entryId, + className: "bpmn-icon-dataflow-data-association-icon", + }; - // replace with an input or output data association depending on the type of the transformation association - if (is(element, consts.INPUT_TRANSFORMATION_ASSOCIATION)) { - associationType = 'bpmn:DataInputAssociation'; - } - replaceConnection(element, associationType, modeling); - }; + // create action to replace the transformation association by a data association + const action = function () { + let associationType = "bpmn:DataOutputAssociation"; - // create menu entry - return { - [entryId]: createMenuEntry(element, definition, translate, replaceElement, action), - }; + // replace with an input or output data association depending on the type of the transformation association + if (is(element, consts.INPUT_TRANSFORMATION_ASSOCIATION)) { + associationType = "bpmn:DataInputAssociation"; } + replaceConnection(element, associationType, modeling); + }; + + // create menu entry + return { + [entryId]: createMenuEntry( + element, + definition, + translate, + replaceElement, + action + ), + }; } + } } DataFlowReplaceMenuProvider.$inject = [ - 'popupMenu', - 'translate', - 'bpmnReplace', - 'modeling', - 'bpmnFactory', - 'moddle', - 'elementRegistry', - 'commandStack', + "popupMenu", + "translate", + "bpmnReplace", + "modeling", + "bpmnFactory", + "moddle", + "elementRegistry", + "commandStack", ]; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceOptions.js b/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceOptions.js index 26732859..b7529e93 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceOptions.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/menu/DataFlowReplaceOptions.js @@ -1,37 +1,37 @@ -import * as consts from '../Constants'; +import * as consts from "../Constants"; // replace options for a BPMN task export const TASK = [ - { - id: 'dataflow-transformation-task', - label: 'Data Transformation Task', - className: 'dataflow-transformation-task-menu-icon', - target: { - type: consts.TRANSFORMATION_TASK - } + { + id: "dataflow-transformation-task", + label: "Data Transformation Task", + className: "dataflow-transformation-task-menu-icon", + target: { + type: consts.TRANSFORMATION_TASK, }, + }, ]; // replace options for a BPMN data store export const DATA_STORE = [ - { - id: 'dataflow-data-store-map', - label: 'Data Store Map', - className: 'dataflow-data-store-map-icon', - target: { - type: consts.DATA_STORE_MAP - } + { + id: "dataflow-data-store-map", + label: "Data Store Map", + className: "dataflow-data-store-map-icon", + target: { + type: consts.DATA_STORE_MAP, }, + }, ]; // replace options for BPMN data object export const DATA_OBJECT = [ - { - id: 'dataflow-data-map-object', - label: 'Data Map Object', - className: 'dataflow-data-map-object-menu-icon', - target: { - type: consts.DATA_MAP_OBJECT - } + { + id: "dataflow-data-map-object", + label: "Data Map Object", + className: "dataflow-data-map-object-menu-icon", + target: { + type: consts.DATA_MAP_OBJECT, }, -]; \ No newline at end of file + }, +]; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js b/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js index bf0fdd1f..4bd7ae3c 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/palette/DataFlowPaletteProvider.js @@ -1,95 +1,93 @@ -import * as consts from '../Constants'; +import * as consts from "../Constants"; export default class DataFlowPaletteProvider { + constructor(bpmnFactory, create, elementFactory, palette, translate) { + this.bpmnFactory = bpmnFactory; + this.create = create; + this.elementFactory = elementFactory; + this.translate = translate; - constructor(bpmnFactory, create, elementFactory, palette, translate) { - this.bpmnFactory = bpmnFactory; - this.create = create; - this.elementFactory = elementFactory; - this.translate = translate; + palette.registerProvider(this); + } - palette.registerProvider(this); - } - - getPaletteEntries() { - return this.createPlanqkServiceTaskEntry(); - } - - createPlanqkServiceTaskEntry() { - const {bpmnFactory, create, elementFactory, translate} = this; + getPaletteEntries() { + return this.createPlanqkServiceTaskEntry(); + } - // start creation of a DataMapObject - function createDataMapObject(event) { - const businessObject = bpmnFactory.create(consts.DATA_MAP_OBJECT); - let shape = elementFactory.createShape({ - type: consts.DATA_MAP_OBJECT, - businessObject: businessObject - }); - create.start(event, shape); - } + createPlanqkServiceTaskEntry() { + const { bpmnFactory, create, elementFactory, translate } = this; - // start creation of a DataStoreMap - function createDataStoreMap(event) { - const businessObject = bpmnFactory.create(consts.DATA_STORE_MAP); - let shape = elementFactory.createShape({ - type: consts.DATA_STORE_MAP, - businessObject: businessObject - }); - create.start(event, shape); - } + // start creation of a DataMapObject + function createDataMapObject(event) { + const businessObject = bpmnFactory.create(consts.DATA_MAP_OBJECT); + let shape = elementFactory.createShape({ + type: consts.DATA_MAP_OBJECT, + businessObject: businessObject, + }); + create.start(event, shape); + } - // start creation of a TransformationTask - function createTransformationTask(event) { - const businessObject = bpmnFactory.create(consts.TRANSFORMATION_TASK); - let shape = elementFactory.createShape({ - type: consts.TRANSFORMATION_TASK, - businessObject: businessObject - }); - create.start(event, shape); - } - + // start creation of a DataStoreMap + function createDataStoreMap(event) { + const businessObject = bpmnFactory.create(consts.DATA_STORE_MAP); + let shape = elementFactory.createShape({ + type: consts.DATA_STORE_MAP, + businessObject: businessObject, + }); + create.start(event, shape); + } - return { - // add separator line to delimit the new group - 'dataflow-separator': { - group: 'dataflowExt', - separator: true - }, - 'create.dataflow-data-map-object': { - group: 'dataflowExt', - className: 'dataflow-data-map-object-palette-icon', - title: translate('Creates a Data Map Object to model data items'), - action: { - click: createDataMapObject, - dragstart: createDataMapObject, - } - }, - 'create.dataflow-data-store-map': { - group: 'dataflowExt', - className: 'dataflow-data-store-map-task-palette-icon', - title: translate('Creates a Data Store Map to model data stores'), - action: { - click: createDataStoreMap, - dragstart: createDataStoreMap, - } - }, - 'create.data-flow-transformation-task': { - group: 'dataflowExt', - className: 'dataflow-transformation-task-palette-icon', - title: translate('Creates a task ot specify data transformations in'), - action: { - click: createTransformationTask, - dragstart: createTransformationTask, - } - } - }; + // start creation of a TransformationTask + function createTransformationTask(event) { + const businessObject = bpmnFactory.create(consts.TRANSFORMATION_TASK); + let shape = elementFactory.createShape({ + type: consts.TRANSFORMATION_TASK, + businessObject: businessObject, + }); + create.start(event, shape); } + + return { + // add separator line to delimit the new group + "dataflow-separator": { + group: "dataflowExt", + separator: true, + }, + "create.dataflow-data-map-object": { + group: "dataflowExt", + className: "dataflow-data-map-object-palette-icon", + title: translate("Creates a Data Map Object to model data items"), + action: { + click: createDataMapObject, + dragstart: createDataMapObject, + }, + }, + "create.dataflow-data-store-map": { + group: "dataflowExt", + className: "dataflow-data-store-map-task-palette-icon", + title: translate("Creates a Data Store Map to model data stores"), + action: { + click: createDataStoreMap, + dragstart: createDataStoreMap, + }, + }, + "create.data-flow-transformation-task": { + group: "dataflowExt", + className: "dataflow-transformation-task-palette-icon", + title: translate("Creates a task ot specify data transformations in"), + action: { + click: createTransformationTask, + dragstart: createTransformationTask, + }, + }, + }; + } } DataFlowPaletteProvider.$inject = [ - 'bpmnFactory', - 'create', - 'elementFactory', - 'palette', - 'translate' + "bpmnFactory", + "create", + "elementFactory", + "palette", + "translate", ]; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/DataFlowPropertiesProvider.js b/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/DataFlowPropertiesProvider.js index 0d779baf..bd30798d 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/DataFlowPropertiesProvider.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/DataFlowPropertiesProvider.js @@ -1,10 +1,10 @@ -import keyValueMap from './KeyValueMap'; -import {is} from 'bpmn-js/lib/util/ModelUtil'; -import {ListGroup} from '@bpmn-io/properties-panel'; -import * as consts from '../Constants'; -import * as configConsts from '../../../editor/configurations/Constants'; -import ConfigurationsProperties from '../../../editor/configurations/ConfigurationsProperties'; -import {getTransformationTaskConfiguration} from '../transf-task-configs/TransformationTaskConfigurations'; +import keyValueMap from "./KeyValueMap"; +import { is } from "bpmn-js/lib/util/ModelUtil"; +import { ListGroup } from "@bpmn-io/properties-panel"; +import * as consts from "../Constants"; +import * as configConsts from "../../../editor/configurations/Constants"; +import ConfigurationsProperties from "../../../editor/configurations/ConfigurationsProperties"; +import { getTransformationTaskConfiguration } from "../transf-task-configs/TransformationTaskConfigurations"; const LOW_PRIORITY = 500; @@ -16,64 +16,82 @@ const LOW_PRIORITY = 500; * @param {Function} translate The translate function of the bpmn-js modeler * @param injector Injector module of the bpmn-js modeler used to load the required dependencies. */ -export default function DataFlowPropertiesProvider(propertiesPanel, translate, injector) { - +export default function DataFlowPropertiesProvider( + propertiesPanel, + translate, + injector +) { + /** + * Return the property groups provided for the given element. + * + * @param element The given element + * + * @return groups middleware + */ + this.getGroups = function (element) { /** - * Return the property groups provided for the given element. + * Return a middleware that adds groups for the properties of the DataFlow elements * - * @param element The given element + * @param {Object[]} groups The default groups for the element * - * @return groups middleware + * @return {Object[]} modified groups */ - this.getGroups = function (element) { - - /** - * Return a middleware that adds groups for the properties of the DataFlow elements - * - * @param {Object[]} groups The default groups for the element - * - * @return {Object[]} modified groups - */ - return function (groups) { - - // add group for displaying the content attribute of a DataMapObject as a key value map - if (is(element, consts.DATA_MAP_OBJECT)) { - groups.push(createDataMapObjectGroup(element, injector, translate)); - } - - // add group for displaying the details attribute of a DataStoreMap as a key value map - if (is(element, consts.DATA_STORE_MAP)) { - groups.push(createDataStoreMapGroup(element, injector, translate)); - } - - // add group for displaying the properties of transformation task and its configurations - if (is(element, consts.TRANSFORMATION_TASK)) { - - // load applied configuration - const selectedConfiguration = getTransformationTaskConfiguration(element.businessObject.get(configConsts.SELECT_CONFIGURATIONS_ID)); - if (selectedConfiguration) { - - // add properties group for properties defined by the configuration - groups.splice(1, 0, createTransformationTaskConfigurationsGroup(element, injector, translate, selectedConfiguration)); - } - - // add entries for the parameters attribute of a transformation task - groups.push(createTransformationTaskGroup(element, injector, translate)); - } - - // add group for displaying the expressions attribute fo the transformation association - if (is(element, consts.TRANSFORMATION_ASSOCIATION)) { - groups.push(createTransformationAssociationGroup(element, injector, translate)); - } - - return groups; - }; + return function (groups) { + // add group for displaying the content attribute of a DataMapObject as a key value map + if (is(element, consts.DATA_MAP_OBJECT)) { + groups.push(createDataMapObjectGroup(element, injector, translate)); + } + + // add group for displaying the details attribute of a DataStoreMap as a key value map + if (is(element, consts.DATA_STORE_MAP)) { + groups.push(createDataStoreMapGroup(element, injector, translate)); + } + + // add group for displaying the properties of transformation task and its configurations + if (is(element, consts.TRANSFORMATION_TASK)) { + // load applied configuration + const selectedConfiguration = getTransformationTaskConfiguration( + element.businessObject.get(configConsts.SELECT_CONFIGURATIONS_ID) + ); + if (selectedConfiguration) { + // add properties group for properties defined by the configuration + groups.splice( + 1, + 0, + createTransformationTaskConfigurationsGroup( + element, + injector, + translate, + selectedConfiguration + ) + ); + } + + // add entries for the parameters attribute of a transformation task + groups.push( + createTransformationTaskGroup(element, injector, translate) + ); + } + + // add group for displaying the expressions attribute fo the transformation association + if (is(element, consts.TRANSFORMATION_ASSOCIATION)) { + groups.push( + createTransformationAssociationGroup(element, injector, translate) + ); + } + + return groups; }; + }; - propertiesPanel.registerProvider(LOW_PRIORITY, this); + propertiesPanel.registerProvider(LOW_PRIORITY, this); } -DataFlowPropertiesProvider.$inject = ['propertiesPanel', 'translate', 'injector']; +DataFlowPropertiesProvider.$inject = [ + "propertiesPanel", + "translate", + "injector", +]; /** * Creates a properties group for displaying the custom properties of a DataFlow data map object. This group contains @@ -85,13 +103,13 @@ DataFlowPropertiesProvider.$inject = ['propertiesPanel', 'translate', 'injector' * @returns {{add: function(*): void, component: ((function(import('../PropertiesPanel').ListGroupDefinition): preact.VNode)|*), id: string, label, items: *}} */ function createDataMapObjectGroup(element, injector, translate) { - const attributeName = consts.CONTENT; - return { - id: 'dataMapObjectProperties', - label: translate('Content'), - component: ListGroup, - ...keyValueMap({element, injector, attributeName}) - }; + const attributeName = consts.CONTENT; + return { + id: "dataMapObjectProperties", + label: translate("Content"), + component: ListGroup, + ...keyValueMap({ element, injector, attributeName }), + }; } /** @@ -104,13 +122,13 @@ function createDataMapObjectGroup(element, injector, translate) { * @returns {{add: function(*): void, component: ((function(import('../PropertiesPanel').ListGroupDefinition): preact.VNode)|*), id: string, label, items: *}} */ function createDataStoreMapGroup(element, injector, translate) { - const attributeName = consts.DETAILS; - return { - id: 'dataStoreMapProperties', - label: translate('Details'), - component: ListGroup, - ...keyValueMap({element, injector, attributeName}) - }; + const attributeName = consts.DETAILS; + return { + id: "dataStoreMapProperties", + label: translate("Details"), + component: ListGroup, + ...keyValueMap({ element, injector, attributeName }), + }; } /** @@ -123,13 +141,13 @@ function createDataStoreMapGroup(element, injector, translate) { * @returns {{add: function(*): void, component: ((function(import('../PropertiesPanel').ListGroupDefinition): preact.VNode)|*), id: string, label, items: *}} */ function createTransformationTaskGroup(element, injector, translate) { - const attributeName = consts.PARAMETERS; - return { - id: 'transformationTaskProperties', - label: translate('Parameters'), - component: ListGroup, - ...keyValueMap({element, injector, attributeName}) - }; + const attributeName = consts.PARAMETERS; + return { + id: "transformationTaskProperties", + label: translate("Parameters"), + component: ListGroup, + ...keyValueMap({ element, injector, attributeName }), + }; } /** @@ -142,13 +160,13 @@ function createTransformationTaskGroup(element, injector, translate) { * @returns {{add: function(*): void, component: ((function(import('../PropertiesPanel').ListGroupDefinition): preact.VNode)|*), id: string, label, items: *}} */ function createTransformationAssociationGroup(element, injector, translate) { - const attributeName = consts.EXPRESSIONS; - return { - id: 'transformationAssociationProperties', - label: translate('Expressions'), - component: ListGroup, - ...keyValueMap({element, injector, attributeName}) - }; + const attributeName = consts.EXPRESSIONS; + return { + id: "transformationAssociationProperties", + label: translate("Expressions"), + component: ListGroup, + ...keyValueMap({ element, injector, attributeName }), + }; } /** @@ -160,11 +178,20 @@ function createTransformationAssociationGroup(element, injector, translate) { * @param configuration The given configuration applied to the element * @returns {{entries: (*), id: string, label}} The created properties group. */ -function createTransformationTaskConfigurationsGroup(element, injector, translate, configuration) { - - return { - id: 'serviceTaskConfigurationsGroupProperties', - label: translate(configuration.groupLabel || 'Configurations Properties'), - entries: ConfigurationsProperties(element, injector, translate, configuration) - }; -} \ No newline at end of file +function createTransformationTaskConfigurationsGroup( + element, + injector, + translate, + configuration +) { + return { + id: "serviceTaskConfigurationsGroupProperties", + label: translate(configuration.groupLabel || "Configurations Properties"), + entries: ConfigurationsProperties( + element, + injector, + translate, + configuration + ), + }; +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueEntry.js b/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueEntry.js index 96a37267..cb7414c1 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueEntry.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueEntry.js @@ -1,109 +1,96 @@ -import React from "@bpmn-io/properties-panel/preact/compat"; -import {TextFieldEntry} from '@bpmn-io/properties-panel'; +import { TextFieldEntry } from "@bpmn-io/properties-panel"; -import {useService} from 'bpmn-js-properties-panel'; +import { useService } from "bpmn-js-properties-panel"; /** * Preact component for an entry for the properties panel which displays a Name and a Value Entry. */ export default function KeyValueEntry(props) { + const { idPrefix, parameter } = props; - const { - idPrefix, - parameter - } = props; - - return [ - { - id: idPrefix + '-key', - component: Key, - idPrefix, - parameter - }, - { - id: idPrefix + '-value', - component: Value, - idPrefix, - parameter - }, - ]; + return [ + { + id: idPrefix + "-key", + component: Key, + idPrefix, + parameter, + }, + { + id: idPrefix + "-value", + component: Value, + idPrefix, + parameter, + }, + ]; } /** * Preact component consisting of a TextFieldEntry for the name property of the given parameter. */ function Key(props) { - const { - idPrefix, - element, - parameter - } = props; + const { idPrefix, element, parameter } = props; - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - // set name property of parameter to the new value - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: parameter, - properties: { - name: value - } - }); - }; + // set name property of parameter to the new value + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: parameter, + properties: { + name: value, + }, + }); + }; - const getValue = (parameter) => { - return parameter.name; - }; + const getValue = (parameter) => { + return parameter.name; + }; - return TextFieldEntry({ - element: parameter, - id: idPrefix + '-name', - label: translate('Name'), - getValue, - setValue, - debounce - }); + return TextFieldEntry({ + element: parameter, + id: idPrefix + "-name", + label: translate("Name"), + getValue, + setValue, + debounce, + }); } /** * Preact component consisting of a TextFieldEntry for the key property of the given parameter. */ function Value(props) { - const { - idPrefix, - element, - parameter - } = props; + const { idPrefix, element, parameter } = props; - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - // set value property of parameter to the new value - const setValue = (value) => { - // return parameter.value = value; - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: parameter, - properties: { - value: value - } - }); - }; + // set value property of parameter to the new value + const setValue = (value) => { + // return parameter.value = value; + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: parameter, + properties: { + value: value, + }, + }); + }; - const getValue = (parameter) => { - return parameter.value; - }; + const getValue = (parameter) => { + return parameter.value; + }; - return TextFieldEntry({ - element: parameter, - id: idPrefix + '-value', - label: translate('Value'), - getValue, - setValue, - debounce - }); -} \ No newline at end of file + return TextFieldEntry({ + element: parameter, + id: idPrefix + "-value", + label: translate("Value"), + getValue, + setValue, + debounce, + }); +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueMap.js b/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueMap.js index 78155f6c..aa29b48e 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueMap.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/properties-panel/KeyValueMap.js @@ -1,10 +1,8 @@ -import { - getBusinessObject -} from 'bpmn-js/lib/util/ModelUtil'; -import KeyValueEntry from './KeyValueEntry'; -import {without} from 'min-dash'; -import * as consts from '../Constants'; -import {nextId} from '../../../editor/util/camunda-utils/ElementUtil'; +import { getBusinessObject } from "bpmn-js/lib/util/ModelUtil"; +import KeyValueEntry from "./KeyValueEntry"; +import { without } from "min-dash"; +import * as consts from "../Constants"; +import { nextId } from "../../../editor/util/camunda-utils/ElementUtil"; /** * Entry of the properties panel which displays a key value map. Entries can be added or removed over the UI of the @@ -17,36 +15,39 @@ import {nextId} from '../../../editor/util/camunda-utils/ElementUtil'; * @returns {{add: ((function(*): void)|*), items: {entries: [{component: function(*): preact.VNode, parameter: *, idPrefix: *, id: string},{component: function(*): preact.VNode, parameter: *, idPrefix: *, id: string}], autoFocusEntry: string, id: string, label, remove: function(*): void}[]}} * @constructor */ -export default function KeyValueMap({element, injector, attributeName}) { +export default function KeyValueMap({ element, injector, attributeName }) { + const bpmnFactory = injector.get("bpmnFactory"), + commandStack = injector.get("commandStack"); - const bpmnFactory = injector.get('bpmnFactory'), - commandStack = injector.get('commandStack'); + // load key value map property + const keyValueMap = element.businessObject.get(attributeName) || []; - // load key value map property - const keyValueMap = element.businessObject.get(attributeName) || []; - - // create a KeyValueEntry for each entry of keyValueMap - const keyValueEntires = keyValueMap.map((keyValueEntry, index) => { - - const id = element.id + '-parameter-' + index; - - return { - id, - label: keyValueEntry.get('name') || '', - entries: KeyValueEntry({ - idPrefix: id, - element, - parameter: keyValueEntry - }), - autoFocusEntry: id + '-name', - remove: removeFactory({commandStack, element, parameter: keyValueEntry, attributeName}) - }; - }); + // create a KeyValueEntry for each entry of keyValueMap + const keyValueEntires = keyValueMap.map((keyValueEntry, index) => { + const id = element.id + "-parameter-" + index; return { - items: keyValueEntires, - add: addFactory({element, bpmnFactory, commandStack, attributeName}) + id, + label: keyValueEntry.get("name") || "", + entries: KeyValueEntry({ + idPrefix: id, + element, + parameter: keyValueEntry, + }), + autoFocusEntry: id + "-name", + remove: removeFactory({ + commandStack, + element, + parameter: keyValueEntry, + attributeName, + }), }; + }); + + return { + items: keyValueEntires, + add: addFactory({ element, bpmnFactory, commandStack, attributeName }), + }; } /** @@ -59,23 +60,28 @@ export default function KeyValueMap({element, injector, attributeName}) { * @param attributeName The attributeName defining the property with the key value map in it. * @returns {(function(*): void)|*} */ -function removeFactory({commandStack, element, keyValueEntry, attributeName}) { - return function (event) { - event.stopPropagation(); +function removeFactory({ + commandStack, + element, + keyValueEntry, + attributeName, +}) { + return function (event) { + event.stopPropagation(); - // get key value map - let keyValueMap = element.businessObject.get(attributeName) || []; + // get key value map + let keyValueMap = element.businessObject.get(attributeName) || []; - // remove the given key value entry - keyValueMap = without(keyValueMap, keyValueEntry); + // remove the given key value entry + keyValueMap = without(keyValueMap, keyValueEntry); - // save updated key value map in the element - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: element.businessObject, - properties: {[attributeName]: keyValueMap}, - }); - }; + // save updated key value map in the element + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: element.businessObject, + properties: { [attributeName]: keyValueMap }, + }); + }; } /** @@ -88,21 +94,24 @@ function removeFactory({commandStack, element, keyValueEntry, attributeName}) { * @param attributeName The name of the property the key value map is saved in. * @returns {(function(*): void)|*} */ -function addFactory({element, bpmnFactory, commandStack, attributeName}) { - return function (event) { - event.stopPropagation(); +function addFactory({ element, bpmnFactory, commandStack, attributeName }) { + return function (event) { + event.stopPropagation(); - const businessObject = getBusinessObject(element); - const keyValueMap = businessObject.get(attributeName); + const businessObject = getBusinessObject(element); + const keyValueMap = businessObject.get(attributeName); - // create a new key value entry - const param = bpmnFactory.create(consts.KEY_VALUE_ENTRY, {name: nextId('Entry_'), value: ''}); + // create a new key value entry + const param = bpmnFactory.create(consts.KEY_VALUE_ENTRY, { + name: nextId("Entry_"), + value: "", + }); - // update key value map - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: {[attributeName]: keyValueMap.concat(param)}, - }); - }; + // update key value map + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { [attributeName]: keyValueMap.concat(param) }, + }); + }; } diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowRenderer.js b/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowRenderer.js index 51ddb976..21bc117a 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowRenderer.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowRenderer.js @@ -1,133 +1,155 @@ import BpmnRenderer from "bpmn-js/lib/draw/BpmnRenderer"; -import * as consts from '../Constants'; -import {attr as svgAttr} from 'tiny-svg'; -import {drawDataElementSVG, drawTaskSVG} from "../../../editor/util/RenderUtilities"; -import {getSVG} from "./DataFlowSVGMap"; -import {extractConfigSVG} from '../../../editor/configurations/ConfigurationsUtil'; +import * as consts from "../Constants"; +import { attr as svgAttr } from "tiny-svg"; +import { + drawDataElementSVG, + drawTaskSVG, +} from "../../../editor/util/RenderUtilities"; +import { getSVG } from "./DataFlowSVGMap"; +import { extractConfigSVG } from "../../../editor/configurations/ConfigurationsUtil"; /** * Custom renderer for the DataFlow elements. Extends the BpmnRenderer of bpmn-js. */ export default class DataFlowRenderer extends BpmnRenderer { - - constructor(config, eventBus, styles, pathMap, canvas, textRenderer) { - super(config, eventBus, styles, pathMap, canvas, textRenderer, 1001); - - // create handlers to render the data flow extension elements - this.dataFlowHandler = { - [consts.DATA_MAP_OBJECT]: function (self, parentGfx, element) { - const task = self.renderer('bpmn:DataObject')(parentGfx, element); - - let svg = extractConfigSVG(element) || getSVG(consts.DATA_TYPE_DATA_MAP_OBJECT); - drawDataElementSVG(parentGfx, svg); - - return task; - }, - [consts.DATA_STORE_MAP]: function (self, parentGfx, element) { - const task = self.renderer('bpmn:DataStoreReference')(parentGfx, element); - - let svg = extractConfigSVG(element) || getSVG(consts.DATA_TYPE_DATA_STORE_MAP); - drawDataElementSVG(parentGfx, svg); - - return task; - }, - [consts.TRANSFORMATION_TASK]: function (self, parentGfx, element) { - const task = self.renderer('bpmn:Task')(parentGfx, element); - - let svg = extractConfigSVG(element) || getSVG(consts.TASK_TYPE_TRANSFORMATION_TASK); - drawTaskSVG(parentGfx, svg); - - return task; - }, - [consts.TRANSFORMATION_ASSOCIATION]: function (self, parentGfx, element) { - const flow = self.renderer('bpmn:DataOutputAssociation')(parentGfx, element); - - svgAttr(flow, { - strokeDasharray: '15, 10', //width, space of the stroke - strokeLinecap: 'square', - }); - - return flow; - }, - [consts.INPUT_TRANSFORMATION_ASSOCIATION]: function (self, parentGfx, element) { - const flow = self.renderer('bpmn:DataInputAssociation')(parentGfx, element); - - svgAttr(flow, { - strokeDasharray: '15, 10', //width, space of the stroke - strokeLinecap: 'square', - }); - - return flow; - }, - [consts.OUTPUT_TRANSFORMATION_ASSOCIATION]: function (self, parentGfx, element) { - const flow = self.renderer('bpmn:DataOutputAssociation')(parentGfx, element); - - svgAttr(flow, { - strokeDasharray: '15, 10', //width, space of the stroke - strokeLinecap: 'square', - }); - - return flow; - } - }; - } - - renderer(type) { - - return this.handlers[type]; + constructor(config, eventBus, styles, pathMap, canvas, textRenderer) { + super(config, eventBus, styles, pathMap, canvas, textRenderer, 1001); + + // create handlers to render the data flow extension elements + this.dataFlowHandler = { + [consts.DATA_MAP_OBJECT]: function (self, parentGfx, element) { + const task = self.renderer("bpmn:DataObject")(parentGfx, element); + + let svg = + extractConfigSVG(element) || getSVG(consts.DATA_TYPE_DATA_MAP_OBJECT); + drawDataElementSVG(parentGfx, svg); + + return task; + }, + [consts.DATA_STORE_MAP]: function (self, parentGfx, element) { + const task = self.renderer("bpmn:DataStoreReference")( + parentGfx, + element + ); + + let svg = + extractConfigSVG(element) || getSVG(consts.DATA_TYPE_DATA_STORE_MAP); + drawDataElementSVG(parentGfx, svg); + + return task; + }, + [consts.TRANSFORMATION_TASK]: function (self, parentGfx, element) { + const task = self.renderer("bpmn:Task")(parentGfx, element); + + let svg = + extractConfigSVG(element) || + getSVG(consts.TASK_TYPE_TRANSFORMATION_TASK); + drawTaskSVG(parentGfx, svg); + + return task; + }, + [consts.TRANSFORMATION_ASSOCIATION]: function (self, parentGfx, element) { + const flow = self.renderer("bpmn:DataOutputAssociation")( + parentGfx, + element + ); + + svgAttr(flow, { + strokeDasharray: "15, 10", //width, space of the stroke + strokeLinecap: "square", + }); + + return flow; + }, + [consts.INPUT_TRANSFORMATION_ASSOCIATION]: function ( + self, + parentGfx, + element + ) { + const flow = self.renderer("bpmn:DataInputAssociation")( + parentGfx, + element + ); + + svgAttr(flow, { + strokeDasharray: "15, 10", //width, space of the stroke + strokeLinecap: "square", + }); + + return flow; + }, + [consts.OUTPUT_TRANSFORMATION_ASSOCIATION]: function ( + self, + parentGfx, + element + ) { + const flow = self.renderer("bpmn:DataOutputAssociation")( + parentGfx, + element + ); + + svgAttr(flow, { + strokeDasharray: "15, 10", //width, space of the stroke + strokeLinecap: "square", + }); + + return flow; + }, + }; + } + + renderer(type) { + return this.handlers[type]; + } + + canRender(element) { + // only return true if handler for rendering is registered + return this.dataFlowHandler[element.type]; + } + + /** + * Draw new shape for the given element based on its type. + * + * @param parentNode Parent of the given element + * @param element The given element + * @returns {*} + */ + drawShape(parentNode, element) { + console.log("Draw Shape of type " + element.type); + + // handle DataFlow elements + if (element.type in this.dataFlowHandler) { + const h = this.dataFlowHandler[element.type]; + + /* jshint -W040 */ + return h(this, parentNode, element); } - - canRender(element) { - - // only return true if handler for rendering is registered - return this.dataFlowHandler[element.type]; + } + + /** + * Draw new connection for the given connection element. + * + * @param parentGfx Parent of the given element + * @param connectionElement The given connection element + * @returns {*} + */ + drawConnection(parentGfx, connectionElement) { + console.log("Draw Connection of type " + connectionElement.type); + + if (connectionElement.type in this.dataFlowHandler) { + let h = this.dataFlowHandler[connectionElement.type]; + return h(this, parentGfx, connectionElement); } - /** - * Draw new shape for the given element based on its type. - * - * @param parentNode Parent of the given element - * @param element The given element - * @returns {*} - */ - drawShape(parentNode, element) { - - console.log("Draw Shape of type " + element.type); - - // handle DataFlow elements - if (element.type in this.dataFlowHandler) { - const h = this.dataFlowHandler[element.type]; - - /* jshint -W040 */ - return h(this, parentNode, element); - } - } - - /** - * Draw new connection for the given connection element. - * - * @param parentGfx Parent of the given element - * @param connectionElement The given connection element - * @returns {*} - */ - drawConnection(parentGfx, connectionElement) { - - console.log("Draw Connection of type " + connectionElement.type); - - if (connectionElement.type in this.dataFlowHandler) { - let h = this.dataFlowHandler[connectionElement.type]; - return h(this, parentGfx, connectionElement); - } - - return super.drawConnection(parentGfx, connectionElement); - } + return super.drawConnection(parentGfx, connectionElement); + } } DataFlowRenderer.$inject = [ - 'config', - 'eventBus', - 'styles', - 'pathMap', - 'canvas', - 'textRenderer' -]; \ No newline at end of file + "config", + "eventBus", + "styles", + "pathMap", + "canvas", + "textRenderer", +]; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowSVGMap.js b/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowSVGMap.js index 1564de86..9c907ba2 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowSVGMap.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/rendering/DataFlowSVGMap.js @@ -1,6 +1,7 @@ import * as consts from "../Constants"; -const DATA_FLOW_MAP_LOGO = ''; +const DATA_FLOW_MAP_LOGO = + ''; /** * Return SVG for the given ID @@ -9,21 +10,23 @@ const DATA_FLOW_MAP_LOGO = '', + }; + svgMap[consts.DATA_TYPE_DATA_MAP_OBJECT] = { + transform: "matrix(0.033, 0, 0, 0.033, 5, 5)", + svg: DATA_FLOW_MAP_LOGO, + }; + svgMap[consts.DATA_TYPE_DATA_STORE_MAP] = { + transform: "matrix(0.034, 0, 0, 0.034, 17, 29)", + svg: DATA_FLOW_MAP_LOGO, + }; - // to insert svgs easily just open them in your browser, copy the outer html and insert it using ctrl + alt + shift + v in intellij to avoid formatting,escaping etc. - // matrix( Scalingfactor, 0, 0, Scalingfactor, shift X, shift y) - // IMPORTANT: ensure that definition Ids for new SVGs are UNIQUE - // viewbox is not required - const svgMap = {}; - svgMap[consts.TASK_TYPE_TRANSFORMATION_TASK] = { - transform: 'matrix(0.17, 0, 0, 0.17, 7, 7)', - svg: '' - }; - svgMap[consts.DATA_TYPE_DATA_MAP_OBJECT] = {transform: 'matrix(0.033, 0, 0, 0.033, 5, 5)', svg: DATA_FLOW_MAP_LOGO}; - svgMap[consts.DATA_TYPE_DATA_STORE_MAP] = { - transform: 'matrix(0.034, 0, 0, 0.034, 17, 29)', - svg: DATA_FLOW_MAP_LOGO - }; - - return svgMap[svgId]; -} \ No newline at end of file + return svgMap[svgId]; +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-extension.json b/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-extension.json index c14c9614..cc98865e 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-extension.json +++ b/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-extension.json @@ -8,7 +8,7 @@ "types": [ { "name": "DataMapObject", - "superClass": [ "bpmn:DataObjectReference" ], + "superClass": ["bpmn:DataObjectReference"], "properties": [ { "name": "content", @@ -19,7 +19,7 @@ }, { "name": "DataStoreMap", - "superClass": [ "bpmn:DataStoreReference" ], + "superClass": ["bpmn:DataStoreReference"], "properties": [ { "name": "details", @@ -30,7 +30,7 @@ }, { "name": "TransformationTask", - "superClass": [ "bpmn:Task" ], + "superClass": ["bpmn:Task"], "properties": [ { "name": "parameters", @@ -41,7 +41,7 @@ }, { "name": "TransformationAssociation", - "superClass": [ "bpmn:Association" ], + "superClass": ["bpmn:Association"], "properties": [ { "name": "expressions", @@ -52,12 +52,12 @@ }, { "name": "InputTransformationAssociation", - "superClass": [ "TransformationAssociation" ], + "superClass": ["TransformationAssociation"], "properties": [] }, { "name": "OutputTransformationAssociation", - "superClass": [ "TransformationAssociation" ], + "superClass": ["TransformationAssociation"], "properties": [] }, { @@ -78,4 +78,4 @@ ], "enumerations": [], "associations": [] -} \ No newline at end of file +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-styles.css b/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-styles.css index 717c87c2..1b267389 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-styles.css +++ b/components/bpmn-q/modeler-component/extensions/data-extension/resources/data-flow-styles.css @@ -1,163 +1,163 @@ .dataflow-transformation-task-icon:before { - content: ""; - width: 5px; - height: 5px; - background-size: contain; - background-image: url("../resources/icons/change-exchange-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin: 0 6px 0 0; + content: ""; + width: 5px; + height: 5px; + background-size: contain; + background-image: url("../resources/icons/change-exchange-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; } .dataflow-transformation-task-icon:hover { - filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); + filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); } .dataflow-data-store-map-icon:before { - content: ""; - width: 30px; - height: 30px; - background-size: contain; - background-image: url("../resources/icons/data-store-map-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin: 0 6px 0 0; + content: ""; + width: 30px; + height: 30px; + background-size: contain; + background-image: url("../resources/icons/data-store-map-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; } .dataflow-data-store-map-icon:hover { - filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); + filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); } .dataflow-data-map-object-icon:before { - content: ""; - width: 10px; - height: 10px; - background-size: contain; - background-image: url("../resources/icons/data-map-object-icon.svg"); - background-repeat: no-repeat; - display: inline-block; + content: ""; + width: 10px; + height: 10px; + background-size: contain; + background-image: url("../resources/icons/data-map-object-icon.svg"); + background-repeat: no-repeat; + display: inline-block; } .dataflow-data-map-object-menu-icon:before { - content: ""; - width: 10px; - height: 10px; - background-size: contain; - background-image: url("../resources/icons/data-map-object-icon.svg"); - background-repeat: no-repeat; - display: inline-block; + content: ""; + width: 10px; + height: 10px; + background-size: contain; + background-image: url("../resources/icons/data-map-object-icon.svg"); + background-repeat: no-repeat; + display: inline-block; } .dataflow-data-map-object-icon:hover { - filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); + filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); } .dataflow-transformation-task-palette-icon { - content: ""; - width: 12px; - height: 12px; - background-size: contain; - background-image: url("../resources/icons/transformation-task.svg"); - background-repeat: no-repeat; - display: inline-block; - transform: scale(0.6); + content: ""; + width: 12px; + height: 12px; + background-size: contain; + background-image: url("../resources/icons/transformation-task.svg"); + background-repeat: no-repeat; + display: inline-block; + transform: scale(0.6); } .dataflow-transformation-task-palette-icon:hover { - filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); + filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); } .dataflow-data-store-map-task-palette-icon { - content: ""; - background-size: contain; - background-image: url("../resources/icons/data-store-map-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - transform: scale(0.6); + content: ""; + background-size: contain; + background-image: url("../resources/icons/data-store-map-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + transform: scale(0.6); } .dataflow-data-store-map-task-palette-icon:hover { - filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); + filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); } .dataflow-data-map-object-palette-icon { - content: ""; - background-size: contain; - background-image: url("../resources/icons/data-map-object-icon.svg"); - background-repeat: no-repeat; - transform: scale(0.8); + content: ""; + background-size: contain; + background-image: url("../resources/icons/data-map-object-icon.svg"); + background-repeat: no-repeat; + transform: scale(0.8); } .dataflow-data-map-object-palette-icon:hover { - filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); + filter: invert(0.5) sepia(1) hue-rotate(165deg) saturate(4) brightness(1); } .dataflow-transformation-association-icon:before { - content: ""; - width: 20px; - height: 20px; - background-size: contain; - background-image: url("../resources/icons/transformation-association-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin: 0 6px 0 0; + content: ""; + width: 20px; + height: 20px; + background-size: contain; + background-image: url("../resources/icons/transformation-association-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; } .dataflow-data-association-icon:before { - content: ""; - width: 20px; - height: 20px; - background-size: contain; - background-image: url("../resources/icons/data-association.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin: 0 6px 0 0; + content: ""; + width: 20px; + height: 20px; + background-size: contain; + background-image: url("../resources/icons/data-association.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; } .dataflow-plugin-icon:before { - content: ""; - width: 15px; - height: 15px; - background-size: contain; - background-image: url("./icons/structure-diagram-icon-or.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin: 0 6px 0 0; + content: ""; + width: 15px; + height: 15px; + background-size: contain; + background-image: url("./icons/structure-diagram-icon-or.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; } .dataflow-update-transformation-task-configs:before { - content: ""; - width: 15px; - height: 15px; - background-size: contain; - background-image: url("./icons/update-icon.svg"); - background-repeat: no-repeat; - display: inline-block; - float: left; - margin: 0 6px 0 0; + content: ""; + width: 15px; + height: 15px; + background-size: contain; + background-image: url("./icons/update-icon.svg"); + background-repeat: no-repeat; + display: inline-block; + float: left; + margin: 0 6px 0 0; } .dataflow-transformation-tasks-menu-icon:before { - content: ""; - width: 5px; - height: 5px; - background-size: contain; - background-image: url("../resources/icons/transformation-task.svg"); - background-repeat: no-repeat; - display: inline-block; + content: ""; + width: 5px; + height: 5px; + background-size: contain; + background-image: url("../resources/icons/transformation-task.svg"); + background-repeat: no-repeat; + display: inline-block; } .dataflow-transformation-task-menu-icon:before { - content: ""; - width: 5px; - height: 5px; - background-size: contain; - background-image: url("../resources/icons/change-exchange-icon.svg"); - background-repeat: no-repeat; - display: inline-block; -} \ No newline at end of file + content: ""; + width: 5px; + height: 5px; + background-size: contain; + background-image: url("../resources/icons/change-exchange-icon.svg"); + background-repeat: no-repeat; + display: inline-block; +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js b/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js index 55037fc3..ff426592 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataFlowRulesProvider.js @@ -1,255 +1,290 @@ -import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules'; +import BpmnRules from "bpmn-js/lib/features/rules/BpmnRules"; +import { is, isAny } from "bpmn-js/lib/features/modeling/util/ModelingUtil"; +import * as consts from "../Constants"; +import { isConnectedWith } from "../../../editor/util/ModellingUtilities"; import { - is, - isAny -} from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; -import * as consts from '../Constants'; -import { isConnectedWith } from '../../../editor/util/ModellingUtilities'; -import { saveFile, setAutoSaveInterval } from '../../../editor/util/IoUtilities'; -import { getModeler } from '../../../editor/ModelerHandler'; -import ace from 'ace-builds'; + saveFile, + setAutoSaveInterval, +} from "../../../editor/util/IoUtilities"; +import { getModeler } from "../../../editor/ModelerHandler"; +import ace from "ace-builds"; import * as editorConfig from "../../../editor/config/EditorConfigManager"; -import { autoSaveFile } from '../../../editor/EditorConstants'; +import { autoSaveFile } from "../../../editor/EditorConstants"; /** * Custom rules provider for the DataFlow elements. Extends the BpmnRules. */ export default class CustomRulesProvider extends BpmnRules { + constructor(eventBus) { + super(eventBus); - constructor(eventBus) { - super(eventBus); + const canConnectDataExtension = this.canConnectDataExtension; + const canConnect = this.canConnect.bind(this); + const canCreate = this.canCreate.bind(this); - const canConnectDataExtension = this.canConnectDataExtension; - const canConnect = this.canConnect.bind(this); - const canCreate = this.canCreate.bind(this); + // persist into local storage whenever copy took place + eventBus.on("copyPaste.elementsCopied", (event) => { + const { tree } = event; - // persist into local storage whenever copy took place - eventBus.on('copyPaste.elementsCopied', event => { - const { tree } = event; - - // persist in local storage, encoded as json - localStorage.setItem('bpmnClipboard', JSON.stringify(tree)); - }); - - /** - * Fired during creation of a new connection (while you selected the target of a connection) - */ - this.addRule('connection.create', 200000, function (context) { - - const source = context.source, - target = context.target; - - return canConnect(source, target); - }); - - /** - * Fired when a connection between two elements is drawn again, e.g. after dragging an element - */ - this.addRule('connection.reconnect', 200000000000, function (context) { - - const source = context.source, - target = context.target; - - let canConnectData = canConnectDataExtension(source, target); - - if (canConnectData || canConnectData === false) { - return canConnectData; - } - }); - - /** - * Fired when a new shape for an element is created - */ - this.addRule('shape.create', 200000000000, function (context) { - - return canCreate( - context.shape, - context.target, - context.source, - context.position - ); - }); - - // save every change when the autosave option is on action - eventBus.on("commandStack.changed", function () { - if (editorConfig.getAutoSaveFileOption() === autoSaveFile.ON_ACTION) { - saveFile(); - } - }); - - // remove interval when autosave option is on action - eventBus.on("autoSaveOptionChanged", function (context) { - if (context.autoSaveFileOption === autoSaveFile.ON_ACTION) { - clearInterval(getModeler().autosaveIntervalId); - } else { - setAutoSaveInterval(); - } - }); - - // update xml viewer on diagram change - eventBus.on("commandStack.changed", function () { - let editor = document.getElementById('editor'); - let aceEditor = ace.edit(editor); - let modeler = getModeler(); - if (modeler) { - if (modeler.xml !== undefined) { - modeler.oldXml = getModeler().xml; - if (getModeler().xml.xml !== undefined) - modeler.oldXml = getModeler().xml.xml; - } - modeler.saveXML({ format: true }).then(function (result) { - if (result.xml !== undefined) { - result = result.xml; - } - aceEditor.setValue(result); - }); - } - }); - } + // persist in local storage, encoded as json + localStorage.setItem("bpmnClipboard", JSON.stringify(tree)); + }); /** - * Returns the type of the connection if the given source and target elements can be connected by the given - * connection element, False else. - * - * @param source The given source element - * @param target The given target element - * @param connection The given connection element + * Fired during creation of a new connection (while you selected the target of a connection) */ - canConnect(source, target, connection) { - console.log('##### can connect'); - - // test connection via transformation association if source or target are DataMapObjects - if (is(source, consts.DATA_MAP_OBJECT) || is(target, consts.DATA_MAP_OBJECT)) { - return this.canConnectDataExtension(source, target); - } + this.addRule("connection.create", 200000, function (context) { + const source = context.source, + target = context.target; - if (!is(connection, 'bpmn:DataAssociation')) { - - // test connection via sequence flow - if (this.canConnectSequenceFlow(source, target)) { - return { type: 'bpmn:SequenceFlow' }; - } - } - - // test connection via super.canConnect - return super.canConnect(source, target, connection); - } + return canConnect(source, target); + }); /** - * Returns True if the given source and target element can be connected via a sequence flow, False else - * - * @param source The given source element - * @param target The given target element + * Fired when a connection between two elements is drawn again, e.g. after dragging an element */ - canConnectSequenceFlow(source, target) { - console.log('##### canConnectSequenceFlow'); + this.addRule("connection.reconnect", 200000000000, function (context) { + const source = context.source, + target = context.target; - // do not allow sequence flow connections with DataMapObjects - if (is(source, consts.DATA_MAP_OBJECT) || is(target, consts.DATA_MAP_OBJECT)) { - return false; - } + let canConnectData = canConnectDataExtension(source, target); - return super.canConnectSequenceFlow(source, target); - } + if (canConnectData || canConnectData === false) { + return canConnectData; + } + }); /** - * Returns the type of the connection if a connection between the given source and target element is possible with a - * transformation association, False else. - * - * @param source The given source element - * @param target The given target element - * @returns {{type: string}|boolean} + * Fired when a new shape for an element is created */ - canConnectDataExtension(source, target) { - console.log('##### can connect data extension'); - - // block outgoing connections from loop, parallel und multi instance markers to data map objects - if (source.businessObject.loopCharacteristics && is(target, consts.DATA_MAP_OBJECT)) { - return false; - } - - // block connections from or to a data map object that is connected with a start event - if ((is(source, consts.DATA_MAP_OBJECT) && isConnectedWith(source, 'bpmn:StartEvent')) - || (is(target, consts.DATA_MAP_OBJECT) && isConnectedWith(target, 'bpmn:StartEvent'))) { - return false; - } - - // add rule for connections via a DataTransformationAssociation - if (isAny(source, [consts.DATA_MAP_OBJECT]) && - isAny(target, [consts.DATA_MAP_OBJECT])) { - console.log('Create connection between DataMapObjects with ' + consts.OUTPUT_TRANSFORMATION_ASSOCIATION); - return { type: consts.OUTPUT_TRANSFORMATION_ASSOCIATION }; - } - - // the normal rules for a DataObject - if (isAny(source, [consts.DATA_MAP_OBJECT]) && isAny(target, ['bpmn:Activity', 'bpmn:ThrowEvent'])) { - console.log('Map to act'); - return { type: 'bpmn:DataInputAssociation' }; - } - if (isAny(target, [consts.DATA_MAP_OBJECT]) && isAny(source, ['bpmn:ThrowEvent'])) { - console.log('Map to act'); - return false; - } - if (isAny(target, [consts.DATA_MAP_OBJECT]) && isAny(source, ['bpmn:Activity', 'bpmn:CatchEvent'])) { - return { type: 'bpmn:DataOutputAssociation' }; - } - if (isAny(source, [consts.DATA_MAP_OBJECT]) && isAny(target, ['bpmn:CatchEvent'])) { - return false; + this.addRule("shape.create", 200000000000, function (context) { + return canCreate( + context.shape, + context.target, + context.source, + context.position + ); + }); + + // save every change when the autosave option is on action + eventBus.on("commandStack.changed", function () { + if (editorConfig.getAutoSaveFileOption() === autoSaveFile.ON_ACTION) { + saveFile(); + } + }); + + // remove interval when autosave option is on action + eventBus.on("autoSaveOptionChanged", function (context) { + if (context.autoSaveFileOption === autoSaveFile.ON_ACTION) { + clearInterval(getModeler().autosaveIntervalId); + } else { + setAutoSaveInterval(); + } + }); + + // update xml viewer on diagram change + eventBus.on("commandStack.changed", function () { + let editor = document.getElementById("editor"); + let aceEditor = ace.edit(editor); + let modeler = getModeler(); + if (modeler) { + if (modeler.xml !== undefined) { + modeler.oldXml = getModeler().xml; + if (getModeler().xml.xml !== undefined) + modeler.oldXml = getModeler().xml.xml; } + modeler.saveXML({ format: true }).then(function (result) { + if (result.xml !== undefined) { + result = result.xml; + } + aceEditor.setValue(result); + }); + } + }); + } + + /** + * Returns the type of the connection if the given source and target elements can be connected by the given + * connection element, False else. + * + * @param source The given source element + * @param target The given target element + * @param connection The given connection element + */ + canConnect(source, target, connection) { + console.log("##### can connect"); + + // test connection via transformation association if source or target are DataMapObjects + if ( + is(source, consts.DATA_MAP_OBJECT) || + is(target, consts.DATA_MAP_OBJECT) + ) { + return this.canConnectDataExtension(source, target); + } - // restrict connections via sequence flow - if (isAny(source, [consts.DATA_MAP_OBJECT]) && - isAny(target, ['bpmn:DataObjectReference', 'bpmn:DataStoreReference', 'bpmn:Gateway'])) { - console.log('No data association between DataObjectMap and DataObjectReference.'); - return false; - } - if (isAny(source, ['bpmn:DataObjectReference', 'bpmn:DataStoreReference', 'bpmn:Gateway']) && - isAny(target, [consts.DATA_MAP_OBJECT])) { - return false; - } + if (!is(connection, "bpmn:DataAssociation")) { + // test connection via sequence flow + if (this.canConnectSequenceFlow(source, target)) { + return { type: "bpmn:SequenceFlow" }; + } } - /** - * Returns True if the given shape can be created in the connection between the source and target element, False else. - * - * @param shape The given shape - * @param target The given target element - * @param source The given source element - * @param position The position where the shape should be created - * @returns {boolean|*|boolean} - */ - canCreate(shape, target, source, position) { - console.log('##### can create'); + // test connection via super.canConnect + return super.canConnect(source, target, connection); + } + + /** + * Returns True if the given source and target element can be connected via a sequence flow, False else + * + * @param source The given source element + * @param target The given target element + */ + canConnectSequenceFlow(source, target) { + console.log("##### canConnectSequenceFlow"); + + // do not allow sequence flow connections with DataMapObjects + if ( + is(source, consts.DATA_MAP_OBJECT) || + is(target, consts.DATA_MAP_OBJECT) + ) { + return false; + } - // do not allow insertion of DataMapObjects - if (is(shape, 'data:DataObjectMapReference')) { + return super.canConnectSequenceFlow(source, target); + } + + /** + * Returns the type of the connection if a connection between the given source and target element is possible with a + * transformation association, False else. + * + * @param source The given source element + * @param target The given target element + * @returns {{type: string}|boolean} + */ + canConnectDataExtension(source, target) { + console.log("##### can connect data extension"); + + // block outgoing connections from loop, parallel und multi instance markers to data map objects + if ( + source.businessObject.loopCharacteristics && + is(target, consts.DATA_MAP_OBJECT) + ) { + return false; + } - if (isAny(target, ['bpmn:SequenceFlow', 'bpmn:DataAssociation'])) { - return false; - } - } + // block connections from or to a data map object that is connected with a start event + if ( + (is(source, consts.DATA_MAP_OBJECT) && + isConnectedWith(source, "bpmn:StartEvent")) || + (is(target, consts.DATA_MAP_OBJECT) && + isConnectedWith(target, "bpmn:StartEvent")) + ) { + return false; + } - return super.canCreate(shape, target, source, position); + // add rule for connections via a DataTransformationAssociation + if ( + isAny(source, [consts.DATA_MAP_OBJECT]) && + isAny(target, [consts.DATA_MAP_OBJECT]) + ) { + console.log( + "Create connection between DataMapObjects with " + + consts.OUTPUT_TRANSFORMATION_ASSOCIATION + ); + return { type: consts.OUTPUT_TRANSFORMATION_ASSOCIATION }; } - /** - * Returns the type of the connection if the given source and target element can be connected via a association connection, False else. - * - * @param source The given source element - * @param target The given target element - * @returns {{type: string}|boolean|*|boolean} - */ - canConnectAssociation(source, target) { - let canConnectData = this.canConnectDataExtension(source, target); + // the normal rules for a DataObject + if ( + isAny(source, [consts.DATA_MAP_OBJECT]) && + isAny(target, ["bpmn:Activity", "bpmn:ThrowEvent"]) + ) { + console.log("Map to act"); + return { type: "bpmn:DataInputAssociation" }; + } + if ( + isAny(target, [consts.DATA_MAP_OBJECT]) && + isAny(source, ["bpmn:ThrowEvent"]) + ) { + console.log("Map to act"); + return false; + } + if ( + isAny(target, [consts.DATA_MAP_OBJECT]) && + isAny(source, ["bpmn:Activity", "bpmn:CatchEvent"]) + ) { + return { type: "bpmn:DataOutputAssociation" }; + } + if ( + isAny(source, [consts.DATA_MAP_OBJECT]) && + isAny(target, ["bpmn:CatchEvent"]) + ) { + return false; + } - if (canConnectData) { - return canConnectData; - } + // restrict connections via sequence flow + if ( + isAny(source, [consts.DATA_MAP_OBJECT]) && + isAny(target, [ + "bpmn:DataObjectReference", + "bpmn:DataStoreReference", + "bpmn:Gateway", + ]) + ) { + console.log( + "No data association between DataObjectMap and DataObjectReference." + ); + return false; + } + if ( + isAny(source, [ + "bpmn:DataObjectReference", + "bpmn:DataStoreReference", + "bpmn:Gateway", + ]) && + isAny(target, [consts.DATA_MAP_OBJECT]) + ) { + return false; + } + } + + /** + * Returns True if the given shape can be created in the connection between the source and target element, False else. + * + * @param shape The given shape + * @param target The given target element + * @param source The given source element + * @param position The position where the shape should be created + * @returns {boolean|*|boolean} + */ + canCreate(shape, target, source, position) { + console.log("##### can create"); + + // do not allow insertion of DataMapObjects + if (is(shape, "data:DataObjectMapReference")) { + if (isAny(target, ["bpmn:SequenceFlow", "bpmn:DataAssociation"])) { + return false; + } + } - return super.canConnectAssociation(source, target); + return super.canCreate(shape, target, source, position); + } + + /** + * Returns the type of the connection if the given source and target element can be connected via a association connection, False else. + * + * @param source The given source element + * @param target The given target element + * @returns {{type: string}|boolean|*|boolean} + */ + canConnectAssociation(source, target) { + let canConnectData = this.canConnectDataExtension(source, target); + + if (canConnectData) { + return canConnectData; } -} -CustomRulesProvider.$inject = [ - 'eventBus', -]; + return super.canConnectAssociation(source, target); + } +} +CustomRulesProvider.$inject = ["eventBus"]; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataReplaceConnectionBehaviour.js b/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataReplaceConnectionBehaviour.js index e4737af2..758659a5 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataReplaceConnectionBehaviour.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/rules/DataReplaceConnectionBehaviour.js @@ -1,187 +1,193 @@ -import { - forEach, - find, - matchPattern -} from 'min-dash'; +import { forEach, find, matchPattern } from "min-dash"; -import { - is, -} from 'bpmn-js/lib/features/modeling/util/ModelingUtil'; +import { is } from "bpmn-js/lib/features/modeling/util/ModelingUtil"; -import inherits from 'inherits-browser'; +import inherits from "inherits-browser"; -import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor'; +import CommandInterceptor from "diagram-js/lib/command/CommandInterceptor"; /** * Custom ReplaceConnectionBehaviour for the DataFlow extension elements. Extends the ReplaceConnectionBehaviour to use * the custom DataFlow rules. */ -export default function DataReplaceConnectionBehavior(eventBus, modeling, bpmnRules, injector, dataFlowRules) { - - CommandInterceptor.call(this, eventBus); - - var dragging = injector.get('dragging', false); - - function fixConnection(connection) { - var source = connection.source, - target = connection.target, - parent = connection.parent; - - // do not do anything if connection - // is already deleted (may happen due to other - // behaviors plugged-in before) - if (!parent) { - return; - } - - var replacementType, - remove; - - /* - * Check if incoming or outgoing connections - * can stay or could be substituted with an - * appropriate replacement. - * - * This holds true for SequenceFlow <> MessageFlow. - */ - if (is(connection, 'bpmn:SequenceFlow')) { - if (!dataFlowRules.canConnectSequenceFlow(source, target)) { - remove = true; - } - - if (bpmnRules.canConnectMessageFlow(source, target)) { - replacementType = 'bpmn:MessageFlow'; - } - } - - // transform message flows into sequence flows, if possible - if (is(connection, 'bpmn:MessageFlow')) { - - if (!bpmnRules.canConnectMessageFlow(source, target)) { - remove = true; - } - - if (dataFlowRules.canConnectSequenceFlow(source, target)) { - replacementType = 'bpmn:SequenceFlow'; - } - } - - if (is(connection, 'bpmn:Association') && !dataFlowRules.canConnectAssociation(source, target)) { - remove = true; - } - - - // remove invalid connection, - // unless it has been removed already - if (remove) { - modeling.removeConnection(connection); - } - - // replace SequenceFlow <> MessageFlow - - if (replacementType) { - modeling.connect(source, target, { - type: replacementType, - waypoints: connection.waypoints.slice() - }); - } +export default function DataReplaceConnectionBehavior( + eventBus, + modeling, + bpmnRules, + injector, + dataFlowRules +) { + CommandInterceptor.call(this, eventBus); + + var dragging = injector.get("dragging", false); + + function fixConnection(connection) { + var source = connection.source, + target = connection.target, + parent = connection.parent; + + // do not do anything if connection + // is already deleted (may happen due to other + // behaviors plugged-in before) + if (!parent) { + return; } - function replaceReconnectedConnection(event) { - - var context = event.context, - connection = context.connection, - source = context.newSource || connection.source, - target = context.newTarget || connection.target, - allowed, - replacement; - - allowed = dataFlowRules.canConnect(source, target); - - if (!allowed || allowed.type === connection.type) { - return; - } + var replacementType, remove; + + /* + * Check if incoming or outgoing connections + * can stay or could be substituted with an + * appropriate replacement. + * + * This holds true for SequenceFlow <> MessageFlow. + */ + if (is(connection, "bpmn:SequenceFlow")) { + if (!dataFlowRules.canConnectSequenceFlow(source, target)) { + remove = true; + } + + if (bpmnRules.canConnectMessageFlow(source, target)) { + replacementType = "bpmn:MessageFlow"; + } + } - replacement = modeling.connect(source, target, { - type: allowed.type, - waypoints: connection.waypoints.slice() - }); + // transform message flows into sequence flows, if possible + if (is(connection, "bpmn:MessageFlow")) { + if (!bpmnRules.canConnectMessageFlow(source, target)) { + remove = true; + } - // remove old connection - modeling.removeConnection(connection); + if (dataFlowRules.canConnectSequenceFlow(source, target)) { + replacementType = "bpmn:SequenceFlow"; + } + } - // replace connection in context to reconnect end/start - context.connection = replacement; + if ( + is(connection, "bpmn:Association") && + !dataFlowRules.canConnectAssociation(source, target) + ) { + remove = true; + } - if (dragging) { - cleanDraggingSelection(connection, replacement); - } + // remove invalid connection, + // unless it has been removed already + if (remove) { + modeling.removeConnection(connection); } - // monkey-patch selection saved in dragging in order to re-select it when operation is finished - function cleanDraggingSelection(oldConnection, newConnection) { - var context = dragging.context(), - previousSelection = context && context.payload.previousSelection, - index; + // replace SequenceFlow <> MessageFlow - // do nothing if not dragging or no selection was present - if (!previousSelection || !previousSelection.length) { - return; - } + if (replacementType) { + modeling.connect(source, target, { + type: replacementType, + waypoints: connection.waypoints.slice(), + }); + } + } - index = previousSelection.indexOf(oldConnection); + function replaceReconnectedConnection(event) { + var context = event.context, + connection = context.connection, + source = context.newSource || connection.source, + target = context.newTarget || connection.target, + allowed, + replacement; - if (index === -1) { - return; - } + allowed = dataFlowRules.canConnect(source, target); - previousSelection.splice(index, 1, newConnection); + if (!allowed || allowed.type === connection.type) { + return; } - // lifecycle hooks + replacement = modeling.connect(source, target, { + type: allowed.type, + waypoints: connection.waypoints.slice(), + }); - this.postExecuted('elements.move', function (context) { + // remove old connection + modeling.removeConnection(connection); - var closure = context.closure, - allConnections = closure.allConnections; + // replace connection in context to reconnect end/start + context.connection = replacement; - forEach(allConnections, fixConnection); - }, true); + if (dragging) { + cleanDraggingSelection(connection, replacement); + } + } - this.preExecute('connection.reconnect', replaceReconnectedConnection); + // monkey-patch selection saved in dragging in order to re-select it when operation is finished + function cleanDraggingSelection(oldConnection, newConnection) { + var context = dragging.context(), + previousSelection = context && context.payload.previousSelection, + index; - this.postExecuted('element.updateProperties', function (event) { - var context = event.context, - properties = context.properties, - element = context.element, - businessObject = element.businessObject, - connection; + // do nothing if not dragging or no selection was present + if (!previousSelection || !previousSelection.length) { + return; + } - // remove condition on change to default - if (properties.default) { - connection = find( - element.outgoing, - matchPattern({id: element.businessObject.default.id}) - ); + index = previousSelection.indexOf(oldConnection); - if (connection) { - modeling.updateProperties(connection, {conditionExpression: undefined}); - } - } + if (index === -1) { + return; + } - // remove default from source on change to conditional - if (properties.conditionExpression && businessObject.sourceRef.default === businessObject) { - modeling.updateProperties(element.source, {default: undefined}); - } - }); + previousSelection.splice(index, 1, newConnection); + } + + // lifecycle hooks + + this.postExecuted( + "elements.move", + function (context) { + var closure = context.closure, + allConnections = closure.allConnections; + + forEach(allConnections, fixConnection); + }, + true + ); + + this.preExecute("connection.reconnect", replaceReconnectedConnection); + + this.postExecuted("element.updateProperties", function (event) { + var context = event.context, + properties = context.properties, + element = context.element, + businessObject = element.businessObject, + connection; + + // remove condition on change to default + if (properties.default) { + connection = find( + element.outgoing, + matchPattern({ id: element.businessObject.default.id }) + ); + + if (connection) { + modeling.updateProperties(connection, { + conditionExpression: undefined, + }); + } + } + + // remove default from source on change to conditional + if ( + properties.conditionExpression && + businessObject.sourceRef.default === businessObject + ) { + modeling.updateProperties(element.source, { default: undefined }); + } + }); } inherits(DataReplaceConnectionBehavior, CommandInterceptor); DataReplaceConnectionBehavior.$inject = [ - 'eventBus', - 'modeling', - 'bpmnRules', - 'injector', - 'dataFlowRules', + "eventBus", + "modeling", + "bpmnRules", + "injector", + "dataFlowRules", ]; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurations.js b/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurations.js index 2ea4827d..bccfcdcc 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurations.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurations.js @@ -1,5 +1,5 @@ -import ConfigurationsEndpoint from '../../../editor/configurations/ConfigurationEndpoint'; -import * as consts from '../Constants'; +import ConfigurationsEndpoint from "../../../editor/configurations/ConfigurationEndpoint"; +import * as consts from "../Constants"; import * as dataConfig from "../config/DataConfigManager"; /** @@ -15,7 +15,7 @@ let endpoint; * @return The list of configurations fot transformation tasks */ export function getTransformationTaskConfigurations() { - return transformationConfigs().getConfigurations(consts.TRANSFORMATION_TASK); + return transformationConfigs().getConfigurations(consts.TRANSFORMATION_TASK); } /** @@ -25,14 +25,14 @@ export function getTransformationTaskConfigurations() { * @returns the configuration with the given id */ export function getTransformationTaskConfiguration(id) { - return transformationConfigs().getConfiguration(id); + return transformationConfigs().getConfiguration(id); } /** * Update the loaded configurations by fetching them form the endpoint again. */ export function updateTransformationTaskConfigurations() { - transformationConfigs().fetchConfigurations(); + transformationConfigs().fetchConfigurations(); } /** @@ -41,8 +41,10 @@ export function updateTransformationTaskConfigurations() { * @return {ConfigurationsEndpoint} the instance of the ConfigurationsEndpoint */ export function transformationConfigs() { - if (!endpoint) { - endpoint = new ConfigurationsEndpoint(dataConfig.getConfigurationsEndpoint()); - } - return endpoint; -} \ No newline at end of file + if (!endpoint) { + endpoint = new ConfigurationsEndpoint( + dataConfig.getConfigurationsEndpoint() + ); + } + return endpoint; +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurationsTab.js b/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurationsTab.js index 862503fe..47e6da7c 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurationsTab.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/transf-task-configs/TransformationTaskConfigurationsTab.js @@ -1,5 +1,5 @@ -import React, {useState} from 'react'; -import {getModeler} from "../../../editor/ModelerHandler"; +import React, { useState } from "react"; +import { getModeler } from "../../../editor/ModelerHandler"; import * as dataConfigManager from "../config/DataConfigManager"; /** @@ -9,35 +9,42 @@ import * as dataConfigManager from "../config/DataConfigManager"; * @constructor */ export default function TransformationTaskConfigurationsTab() { + const [configurationsEndpoint, setConfigurationsEndpoint] = useState( + dataConfigManager.getConfigurationsEndpoint() + ); - const [configurationsEndpoint, setConfigurationsEndpoint] = useState(dataConfigManager.getConfigurationsEndpoint()); + // save changed endpoint url if the modal is closed + TransformationTaskConfigurationsTab.prototype.onClose = () => { + dataConfigManager.setConfigurationsEndpoint(configurationsEndpoint); + }; - // save changed endpoint url if the modal is closed - TransformationTaskConfigurationsTab.prototype.onClose = () => { - dataConfigManager.setConfigurationsEndpoint(configurationsEndpoint); - }; - - return (<> -

Data Configurations endpoint configuration:

- - - - - - - -
Configurations Endpoint - setConfigurationsEndpoint(event.target.value)}/> -
- ); + return ( + <> +

Data Configurations endpoint configuration:

+ + + + + + + +
Configurations Endpoint + + setConfigurationsEndpoint(event.target.value) + } + /> +
+ + ); } TransformationTaskConfigurationsTab.prototype.config = () => { - const modeler = getModeler(); + const modeler = getModeler(); - modeler.config.transformationTaskConfigurationsEndpointChanged = dataConfigManager.getConfigurationsEndpoint(); -}; \ No newline at end of file + modeler.config.transformationTaskConfigurationsEndpointChanged = + dataConfigManager.getConfigurationsEndpoint(); +}; diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/transformation/TransformationManager.js b/components/bpmn-q/modeler-component/extensions/data-extension/transformation/TransformationManager.js index 9671fcb8..59ad87df 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/transformation/TransformationManager.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/transformation/TransformationManager.js @@ -1,20 +1,23 @@ -import { is } from 'bpmn-js/lib/util/ModelUtil'; -import { getXml } from '../../../editor/util/IoUtilities'; -import { createTempModelerFromXml } from '../../../editor/ModelerHandler'; -import * as consts from '../Constants'; +import { is } from "bpmn-js/lib/util/ModelUtil"; +import { getXml } from "../../../editor/util/IoUtilities"; +import { createTempModelerFromXml } from "../../../editor/ModelerHandler"; +import * as consts from "../Constants"; import { - getAllElementsForProcess, - getAllElementsInProcess, - insertShape -} from '../../../editor/util/TransformationUtilities'; + getAllElementsForProcess, + getAllElementsInProcess, + insertShape, +} from "../../../editor/util/TransformationUtilities"; import { - addCamundaInputMapParameter, - addCamundaInputParameter, - addCamundaOutputMapParameter, - addFormField, findSequenceFlowConnection, getDocumentation, - getRootProcess, setDocumentation, -} from '../../../editor/util/ModellingUtilities'; -import { layout } from '../../quantme/replacement/layouter/Layouter'; + addCamundaInputMapParameter, + addCamundaInputParameter, + addCamundaOutputMapParameter, + addFormField, + findSequenceFlowConnection, + getDocumentation, + getRootProcess, + setDocumentation, +} from "../../../editor/util/ModellingUtilities"; +import { layout } from "../../quantme/replacement/layouter/Layouter"; /** * Replace data flow extensions with camunda bpmn elements so that it complies with the standard @@ -23,182 +26,260 @@ import { layout } from '../../quantme/replacement/layouter/Layouter'; * @returns {Promise<{xml: *, status: string}|{cause: string, status: string}>} */ export async function startDataFlowReplacementProcess(xml) { - let modeler = await createTempModelerFromXml(xml); - let elementRegistry = modeler.get('elementRegistry'); - let modeling = modeler.get('modeling'); - - // get root element of the current diagram - const definitions = modeler.getDefinitions(); - const rootProcess = getRootProcess(definitions); - - console.log(rootProcess); - - if (typeof rootProcess === 'undefined') { - - console.log('Unable to retrieve root process element from definitions!'); - return { status: 'failed', cause: 'Unable to retrieve root process element from definitions!' }; + let modeler = await createTempModelerFromXml(xml); + let elementRegistry = modeler.get("elementRegistry"); + let modeling = modeler.get("modeling"); + + // get root element of the current diagram + const definitions = modeler.getDefinitions(); + const rootProcess = getRootProcess(definitions); + + console.log(rootProcess); + + if (typeof rootProcess === "undefined") { + console.log("Unable to retrieve root process element from definitions!"); + return { + status: "failed", + cause: "Unable to retrieve root process element from definitions!", + }; + } + + // Mark process as executable + rootProcess.isExecutable = true; + + const bpmnFactory = modeler.get("bpmnFactory"); + const moddle = modeler.get("moddle"); + + // for each transformation association + const transformationAssociations = elementRegistry.filter(function (element) { + console.log(element.id); + return is(element, consts.TRANSFORMATION_ASSOCIATION); + }); + console.log( + "Found " + + transformationAssociations.length + + " TransformationAssociations." + ); + + let targetDataMapObject, + sourceDataMapObject, + targetActivityElement, + targetContent; + + for (let transformationAssociation of transformationAssociations) { + // if source === DataMapObject: expressions als inputs im target + if ( + transformationAssociation.source.type === consts.DATA_MAP_OBJECT && + transformationAssociation.target.type !== consts.DATA_MAP_OBJECT + ) { + targetActivityElement = transformationAssociation.target; + + const expressions = transformationAssociation.businessObject.get( + consts.EXPRESSIONS + ); + for (let expression of expressions) { + addCamundaInputParameter( + targetActivityElement.businessObject, + expression.name, + expression.value, + bpmnFactory + ); + } } - // Mark process as executable - rootProcess.isExecutable = true; - - const bpmnFactory = modeler.get('bpmnFactory'); - const moddle = modeler.get('moddle'); - - // for each transformation association - const transformationAssociations = elementRegistry.filter(function (element) { - console.log(element.id); - return is(element, consts.TRANSFORMATION_ASSOCIATION); - }); - console.log('Found ' + transformationAssociations.length + ' TransformationAssociations.'); - - let targetDataMapObject, + // if target && source === DataMapObject: add expressions to content of target data map object + if ( + transformationAssociation.source.type === consts.DATA_MAP_OBJECT && + transformationAssociation.target.type === consts.DATA_MAP_OBJECT + ) { + targetDataMapObject = transformationAssociation.target; + sourceDataMapObject = transformationAssociation.source; + targetContent = + targetDataMapObject.businessObject.get(consts.CONTENT) || []; + + const expressions = transformationAssociation.businessObject.get( + consts.EXPRESSIONS + ); + for (let expression of expressions) { + targetContent.push( + bpmnFactory.create(consts.KEY_VALUE_ENTRY, { + name: expression.name, + value: expression.value, + }) + ); + } + + // mark target data map objects as created through a transformation association + sourceDataMapObject.businessObject.createsThroughTransformation = true; + targetDataMapObject.businessObject.createdByTransformation = true; + + // document the transformation in the source and target elements + const currentSourceDoc = + getDocumentation(sourceDataMapObject.businessObject) || ""; + setDocumentation( sourceDataMapObject, - targetActivityElement, - targetContent; - - for (let transformationAssociation of transformationAssociations) { - - // if source === DataMapObject: expressions als inputs im target - if ((transformationAssociation.source.type === consts.DATA_MAP_OBJECT) && (transformationAssociation.target.type !== consts.DATA_MAP_OBJECT)) { - targetActivityElement = transformationAssociation.target; - - const expressions = transformationAssociation.businessObject.get(consts.EXPRESSIONS); - for (let expression of expressions) { - addCamundaInputParameter(targetActivityElement.businessObject, expression.name, expression.value, bpmnFactory); - } - } - - // if target && source === DataMapObject: add expressions to content of target data map object - if ((transformationAssociation.source.type === consts.DATA_MAP_OBJECT) && (transformationAssociation.target.type === consts.DATA_MAP_OBJECT)) { - targetDataMapObject = transformationAssociation.target; - sourceDataMapObject = transformationAssociation.source; - targetContent = targetDataMapObject.businessObject.get(consts.CONTENT) || []; - - const expressions = transformationAssociation.businessObject.get(consts.EXPRESSIONS); - for (let expression of expressions) { - targetContent.push(bpmnFactory.create(consts.KEY_VALUE_ENTRY, { - name: expression.name, - value: expression.value - })); - } - - // mark target data map objects as created through a transformation association - sourceDataMapObject.businessObject.createsThroughTransformation = true; - targetDataMapObject.businessObject.createdByTransformation = true; - - // document the transformation in the source and target elements - const currentSourceDoc = getDocumentation(sourceDataMapObject.businessObject) || ''; - setDocumentation(sourceDataMapObject, currentSourceDoc.concat(createTransformationSourceDocs(transformationAssociation)), bpmnFactory); - - const currentTargetDoc = getDocumentation(targetDataMapObject.businessObject) || ''; - setDocumentation(targetDataMapObject, currentTargetDoc.concat(createTransformationTargetDocs(transformationAssociation)), bpmnFactory); - } + currentSourceDoc.concat( + createTransformationSourceDocs(transformationAssociation) + ), + bpmnFactory + ); + + const currentTargetDoc = + getDocumentation(targetDataMapObject.businessObject) || ""; + setDocumentation( + targetDataMapObject, + currentTargetDoc.concat( + createTransformationTargetDocs(transformationAssociation) + ), + bpmnFactory + ); } - - // for each data association - const dataAssociations = elementRegistry.filter(function (element) { - return is(element, 'bpmn:DataAssociation'); - }); - console.log('Found ' + dataAssociations.length + ' DataAssociations.'); - - let source, - target, - dataMapObject, - activity, - businessObject; - - for (let dataAssociation of dataAssociations) { - source = dataAssociation.source; - target = dataAssociation.target; - - // if source === DataMapObject: content als input in target activity - if (source.type === consts.DATA_MAP_OBJECT) { - activity = target; - dataMapObject = source; - businessObject = dataMapObject.businessObject; - - addCamundaInputMapParameter(activity.businessObject, businessObject.name, businessObject.get(consts.CONTENT), bpmnFactory); - } - - // if target === DataMapObject: content als output in source - if (target.type === consts.DATA_MAP_OBJECT) { - dataMapObject = target; - activity = source; - businessObject = dataMapObject.businessObject; - - if (source.type === 'bpmn:StartEvent') { - - const name = businessObject.get('name'); - - for (let c of businessObject.get(consts.CONTENT)) { - let formField = - { - 'defaultValue': c.value, - 'id': name + '.' + c.name, - 'label': name + '.' + c.name, - 'type': 'string' - }; - addFormField(activity.id, formField, elementRegistry, moddle, modeling); - } - - } else { - addCamundaOutputMapParameter(activity.businessObject, businessObject.name, businessObject.get(consts.CONTENT), bpmnFactory); - } - } - } - - const globalProcessVariables = {}; - - // transform DataMapObjects to data objects - let transformationSuccess = transformDataMapObjects(rootProcess, definitions, globalProcessVariables, modeler); - if (!transformationSuccess) { - const failureMessage = `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + transformationSuccess.failedData.id + ' failed. Aborting process!'; - console.log(failureMessage); - return { - status: 'failed', - cause: failureMessage, - }; + } + + // for each data association + const dataAssociations = elementRegistry.filter(function (element) { + return is(element, "bpmn:DataAssociation"); + }); + console.log("Found " + dataAssociations.length + " DataAssociations."); + + let source, target, dataMapObject, activity, businessObject; + + for (let dataAssociation of dataAssociations) { + source = dataAssociation.source; + target = dataAssociation.target; + + // if source === DataMapObject: content als input in target activity + if (source.type === consts.DATA_MAP_OBJECT) { + activity = target; + dataMapObject = source; + businessObject = dataMapObject.businessObject; + + addCamundaInputMapParameter( + activity.businessObject, + businessObject.name, + businessObject.get(consts.CONTENT), + bpmnFactory + ); } - // transform DataStoreMap to data stores - transformationSuccess = transformDataStoreMaps(rootProcess, definitions, globalProcessVariables, modeler); - if (!transformationSuccess) { - const failureMessage = `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + transformationSuccess.failedData.id + ' failed. Aborting process!'; - console.log(failureMessage); - return { - status: 'failed', - cause: failureMessage, - }; + // if target === DataMapObject: content als output in source + if (target.type === consts.DATA_MAP_OBJECT) { + dataMapObject = target; + activity = source; + businessObject = dataMapObject.businessObject; + + if (source.type === "bpmn:StartEvent") { + const name = businessObject.get("name"); + + for (let c of businessObject.get(consts.CONTENT)) { + let formField = { + defaultValue: c.value, + id: name + "." + c.name, + label: name + "." + c.name, + type: "string", + }; + addFormField( + activity.id, + formField, + elementRegistry, + moddle, + modeling + ); + } + } else { + addCamundaOutputMapParameter( + activity.businessObject, + businessObject.name, + businessObject.get(consts.CONTENT), + bpmnFactory + ); + } } - - // transform TransformationTasks to service tasks - transformationSuccess = transformTransformationTask(rootProcess, definitions, globalProcessVariables, modeler); + } + + const globalProcessVariables = {}; + + // transform DataMapObjects to data objects + let transformationSuccess = transformDataMapObjects( + rootProcess, + definitions, + globalProcessVariables, + modeler + ); + if (!transformationSuccess) { + const failureMessage = + `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + + transformationSuccess.failedData.id + + " failed. Aborting process!"; + console.log(failureMessage); + return { + status: "failed", + cause: failureMessage, + }; + } + + // transform DataStoreMap to data stores + transformationSuccess = transformDataStoreMaps( + rootProcess, + definitions, + globalProcessVariables, + modeler + ); + if (!transformationSuccess) { + const failureMessage = + `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + + transformationSuccess.failedData.id + + " failed. Aborting process!"; + console.log(failureMessage); + return { + status: "failed", + cause: failureMessage, + }; + } + + // transform TransformationTasks to service tasks + transformationSuccess = transformTransformationTask( + rootProcess, + definitions, + globalProcessVariables, + modeler + ); + if (!transformationSuccess) { + const failureMessage = + `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + + transformationSuccess.failedData.id + + " failed. Aborting process!"; + console.log(failureMessage); + return { + status: "failed", + cause: failureMessage, + }; + } + + if (Object.entries(globalProcessVariables).length > 0) { + transformationSuccess = createProcessContextVariablesTask( + globalProcessVariables, + rootProcess, + definitions, + modeler + ); if (!transformationSuccess) { - const failureMessage = `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + transformationSuccess.failedData.id + ' failed. Aborting process!'; - console.log(failureMessage); - return { - status: 'failed', - cause: failureMessage, - }; + const failureMessage = + `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + + transformationSuccess.failedData.id + + " failed. Aborting process!"; + console.log(failureMessage); + return { + status: "failed", + cause: failureMessage, + }; } + } - if (Object.entries(globalProcessVariables).length > 0) { - transformationSuccess = createProcessContextVariablesTask(globalProcessVariables, rootProcess, definitions, modeler); - if (!transformationSuccess) { - const failureMessage = `Replacement of Data modeling construct ${transformationSuccess.failedData.type} with Id ` + transformationSuccess.failedData.id + ' failed. Aborting process!'; - console.log(failureMessage); - return { - status: 'failed', - cause: failureMessage, - }; - } - } - - layout(modeling, elementRegistry, rootProcess); + layout(modeling, elementRegistry, rootProcess); - const transformedXML = await getXml(modeler); - return { status: 'transformed', xml: transformedXML }; + const transformedXML = await getXml(modeler); + return { status: "transformed", xml: transformedXML }; } /** @@ -213,59 +294,79 @@ export async function startDataFlowReplacementProcess(xml) { * @return {{success: boolean}|{success: boolean, failedData: *}} Success flag with True if transformation was successful, * False else with details in failedData. */ -function transformDataMapObjects(rootProcess, definitions, processContextVariables, modeler) { - let bpmnFactory = modeler.get('bpmnFactory'); - let elementRegistry = modeler.get('elementRegistry'); - - // get all data map objects of the current process including subprocesses - const dataObjectMaps = getAllElementsInProcess(rootProcess, elementRegistry, consts.DATA_MAP_OBJECT); - console.log('Found ' + dataObjectMaps.length + ' DataObjectMapReferences to replace.'); - - // replace all data map objects with data objects and transform the content attribute - for (let dataElement of dataObjectMaps) { - - const dataMapObjectBo = dataElement.element; - const dataMapObjectElement = elementRegistry.get(dataMapObjectBo.id); - - const isUsedBeforeInit = isDataMapObjectUsedBeforeInitialized(dataMapObjectElement, elementRegistry); - - // check if the content of the data map object has to be published in process content - if (dataMapObjectBo.createdByTransformation - || dataMapObjectBo.createsThroughTransformation - || !dataMapObjectElement.incoming - || dataMapObjectElement.incoming.length === 0 - || isUsedBeforeInit) { - - // const startEvents = getStartEvents(); - const processElement = dataElement.parent; - - if (!processContextVariables[processElement.id]) { - processContextVariables[processElement.id] = []; - } - - // publish content of the data map object as process variable in process context - processContextVariables[processElement.id].push({ - name: dataMapObjectBo.name, - map: dataMapObjectBo.get(consts.CONTENT) - }); - } - - // replace data map object by data object - const dataObject = bpmnFactory.create('bpmn:DataObjectReference'); - const result = insertShape(definitions, dataObject.parent, dataObject, {}, true, modeler, dataMapObjectBo); +function transformDataMapObjects( + rootProcess, + definitions, + processContextVariables, + modeler +) { + let bpmnFactory = modeler.get("bpmnFactory"); + let elementRegistry = modeler.get("elementRegistry"); + + // get all data map objects of the current process including subprocesses + const dataObjectMaps = getAllElementsInProcess( + rootProcess, + elementRegistry, + consts.DATA_MAP_OBJECT + ); + console.log( + "Found " + dataObjectMaps.length + " DataObjectMapReferences to replace." + ); + + // replace all data map objects with data objects and transform the content attribute + for (let dataElement of dataObjectMaps) { + const dataMapObjectBo = dataElement.element; + const dataMapObjectElement = elementRegistry.get(dataMapObjectBo.id); + + const isUsedBeforeInit = isDataMapObjectUsedBeforeInitialized( + dataMapObjectElement, + elementRegistry + ); + + // check if the content of the data map object has to be published in process content + if ( + dataMapObjectBo.createdByTransformation || + dataMapObjectBo.createsThroughTransformation || + !dataMapObjectElement.incoming || + dataMapObjectElement.incoming.length === 0 || + isUsedBeforeInit + ) { + // const startEvents = getStartEvents(); + const processElement = dataElement.parent; + + if (!processContextVariables[processElement.id]) { + processContextVariables[processElement.id] = []; + } - if (result.success) { + // publish content of the data map object as process variable in process context + processContextVariables[processElement.id].push({ + name: dataMapObjectBo.name, + map: dataMapObjectBo.get(consts.CONTENT), + }); + } - // set documentation property of the data object to document the data map object it replaces - const currentDoc = getDocumentation(dataMapObjectBo) || ''; - const dataDoc = createDataMapObjectDocs(dataMapObjectBo); - setDocumentation(result.element, currentDoc.concat(dataDoc), bpmnFactory); - } else { - return { success: false, failedData: dataMapObjectBo }; - } + // replace data map object by data object + const dataObject = bpmnFactory.create("bpmn:DataObjectReference"); + const result = insertShape( + definitions, + dataObject.parent, + dataObject, + {}, + true, + modeler, + dataMapObjectBo + ); + if (result.success) { + // set documentation property of the data object to document the data map object it replaces + const currentDoc = getDocumentation(dataMapObjectBo) || ""; + const dataDoc = createDataMapObjectDocs(dataMapObjectBo); + setDocumentation(result.element, currentDoc.concat(dataDoc), bpmnFactory); + } else { + return { success: false, failedData: dataMapObjectBo }; } - return { success: true }; + } + return { success: true }; } /** @@ -279,23 +380,40 @@ function transformDataMapObjects(rootProcess, definitions, processContextVariabl * @return {{success: boolean}|{success: boolean, failedData: *}} Success flag with True if transformation was successful, * False else with details in failedData. */ -function transformDataStoreMaps(rootProcess, definitions, processContextVariables, modeler) { - let elementRegistry = modeler.get('elementRegistry'); - - // get all data store maps of the current process including the data store maps in subprocesses - const dataStoreElements = getAllElementsInProcess(rootProcess, elementRegistry, consts.DATA_STORE_MAP); - console.log('Found ' + dataStoreElements.length + ' DataObjectMapReferences to replace.'); - - // replace all data store maps and transform their details attributes - for (let dataElement of dataStoreElements) { - const result = transformDataStoreMap(dataElement.element, dataElement.parent, definitions, processContextVariables, modeler); - - if (!result.success) { - // break transformation and propagate failure - return { success: false, failedData: dataElement.element }; - } +function transformDataStoreMaps( + rootProcess, + definitions, + processContextVariables, + modeler +) { + let elementRegistry = modeler.get("elementRegistry"); + + // get all data store maps of the current process including the data store maps in subprocesses + const dataStoreElements = getAllElementsInProcess( + rootProcess, + elementRegistry, + consts.DATA_STORE_MAP + ); + console.log( + "Found " + dataStoreElements.length + " DataObjectMapReferences to replace." + ); + + // replace all data store maps and transform their details attributes + for (let dataElement of dataStoreElements) { + const result = transformDataStoreMap( + dataElement.element, + dataElement.parent, + definitions, + processContextVariables, + modeler + ); + + if (!result.success) { + // break transformation and propagate failure + return { success: false, failedData: dataElement.element }; } - return { success: true }; + } + return { success: true }; } /** @@ -310,38 +428,49 @@ function transformDataStoreMaps(rootProcess, definitions, processContextVariable * @return {{success: boolean}|{success: boolean, failedData: *}} Success flag with True if transformation was successful, * False else with details in failedData. */ -export function transformDataStoreMap(dataStoreMap, parentElement, definitions, processContextVariables, modeler) { - - const bpmnFactory = modeler.get('bpmnFactory'); - - const processElement = parentElement; - if (!processContextVariables[processElement.id]) { - processContextVariables[processElement.id] = []; - } - - // publish details of the data store map as process variable in process context - processContextVariables[processElement.id].push({ - name: dataStoreMap.name, - map: dataStoreMap.get(consts.DETAILS) - }); - - // replace data store map by data store - const dataStore = bpmnFactory.create('bpmn:DataStoreReference'); - const result = insertShape(definitions, dataStore.parent, dataStore, {}, true, modeler, dataStoreMap); - - if (result.success) { - - // set documentation property of the data store to document the data store map it replaces - const currentDoc = getDocumentation(dataStoreMap) || ''; - const dataDoc = createDataStoreMapDocs(dataStoreMap); - setDocumentation(result.element, currentDoc.concat(dataDoc), bpmnFactory); - } else { - return { success: false, failedData: dataStoreMap }; - } - return { success: true }; +export function transformDataStoreMap( + dataStoreMap, + parentElement, + definitions, + processContextVariables, + modeler +) { + const bpmnFactory = modeler.get("bpmnFactory"); + + const processElement = parentElement; + if (!processContextVariables[processElement.id]) { + processContextVariables[processElement.id] = []; + } + + // publish details of the data store map as process variable in process context + processContextVariables[processElement.id].push({ + name: dataStoreMap.name, + map: dataStoreMap.get(consts.DETAILS), + }); + + // replace data store map by data store + const dataStore = bpmnFactory.create("bpmn:DataStoreReference"); + const result = insertShape( + definitions, + dataStore.parent, + dataStore, + {}, + true, + modeler, + dataStoreMap + ); + + if (result.success) { + // set documentation property of the data store to document the data store map it replaces + const currentDoc = getDocumentation(dataStoreMap) || ""; + const dataDoc = createDataStoreMapDocs(dataStoreMap); + setDocumentation(result.element, currentDoc.concat(dataDoc), bpmnFactory); + } else { + return { success: false, failedData: dataStoreMap }; + } + return { success: true }; } - /** * Transform TransformationTasks to service tasks. Add the parameters attribute of the TransformationTask as a camunda map * inputs of the service task. @@ -353,31 +482,56 @@ export function transformDataStoreMap(dataStoreMap, parentElement, definitions, * @return {{success: boolean}|{success: boolean, failedData: *}} Success flag with True if transformation was successful, * False else with details in failedData. */ -function transformTransformationTask(rootProcess, definitions, processContextVariables, modeler) { - let bpmnFactory = modeler.get('bpmnFactory'); - let elementRegistry = modeler.get('elementRegistry'); - - // get all transformation task of the root process including the tasks in subprocesses - const transformationTasks = getAllElementsInProcess(rootProcess, elementRegistry, consts.TRANSFORMATION_TASK); - console.log('Found ' + transformationTasks.length + ' DataObjectMapReferences to replace.'); - - // transform each task into a service task and add the parameters attribute to the inputs of the service task. - for (let taskElement of transformationTasks) { - - const transformationTask = taskElement.element; - - // replace transformation task by new service task - const serviceTask = bpmnFactory.create('bpmn:ServiceTask'); - const result = insertShape(definitions, serviceTask.parent, serviceTask, {}, true, modeler, transformationTask); - - if (!result.success) { - return { success: false, failedData: transformationTask }; - } - - // add parameters attribute as camunda map to service task inputs - addCamundaInputMapParameter(result.element.businessObject, consts.PARAMETERS, transformationTask.get(consts.PARAMETERS), bpmnFactory); +function transformTransformationTask( + rootProcess, + definitions, + processContextVariables, + modeler +) { + let bpmnFactory = modeler.get("bpmnFactory"); + let elementRegistry = modeler.get("elementRegistry"); + + // get all transformation task of the root process including the tasks in subprocesses + const transformationTasks = getAllElementsInProcess( + rootProcess, + elementRegistry, + consts.TRANSFORMATION_TASK + ); + console.log( + "Found " + + transformationTasks.length + + " DataObjectMapReferences to replace." + ); + + // transform each task into a service task and add the parameters attribute to the inputs of the service task. + for (let taskElement of transformationTasks) { + const transformationTask = taskElement.element; + + // replace transformation task by new service task + const serviceTask = bpmnFactory.create("bpmn:ServiceTask"); + const result = insertShape( + definitions, + serviceTask.parent, + serviceTask, + {}, + true, + modeler, + transformationTask + ); + + if (!result.success) { + return { success: false, failedData: transformationTask }; } - return { success: true }; + + // add parameters attribute as camunda map to service task inputs + addCamundaInputMapParameter( + result.element.businessObject, + consts.PARAMETERS, + transformationTask.get(consts.PARAMETERS), + bpmnFactory + ); + } + return { success: true }; } /** @@ -390,36 +544,60 @@ function transformTransformationTask(rootProcess, definitions, processContextVar * @param modeler The modeler containing the workflow to transform * @return {{success: boolean}} True if the ProcessVariablesTask could be successfully created, False else. */ -export function createProcessContextVariablesTask(processContextVariables, rootProcess, definitions, modeler) { - const elementRegistry = modeler.get('elementRegistry'); - const bpmnFactory = modeler.get('bpmnFactory'); - const modeling = modeler.get('modeling'); - - // add for each process or subprocess a new task to create process variables - for (let processEntry of Object.entries(processContextVariables)) { - const processId = processEntry[0]; - const processBo = elementRegistry.get(processId).businessObject; - - const startEvents = getAllElementsForProcess(processBo, elementRegistry, 'bpmn:StartEvent'); - - console.log(`Found ${startEvents && startEvents.length} StartEvents in process ${processId}`); - console.log(startEvents); - - // add ProcessVariablesTask after each start event - for (let event of startEvents) { - const startEventBo = event.element; - const startEventElement = elementRegistry.get(startEventBo.id); - - const newTaskBo = getProcessContextVariablesTask(startEventElement, event.parent, bpmnFactory, modeling, elementRegistry); - - // add camunda map to outputs for each entry - for (let processVariable of processContextVariables[processId]) { - addCamundaOutputMapParameter(newTaskBo, processVariable.name, processVariable.map, bpmnFactory); - } - } +export function createProcessContextVariablesTask( + processContextVariables, + rootProcess, + definitions, + modeler +) { + const elementRegistry = modeler.get("elementRegistry"); + const bpmnFactory = modeler.get("bpmnFactory"); + const modeling = modeler.get("modeling"); + + // add for each process or subprocess a new task to create process variables + for (let processEntry of Object.entries(processContextVariables)) { + const processId = processEntry[0]; + const processBo = elementRegistry.get(processId).businessObject; + + const startEvents = getAllElementsForProcess( + processBo, + elementRegistry, + "bpmn:StartEvent" + ); + + console.log( + `Found ${ + startEvents && startEvents.length + } StartEvents in process ${processId}` + ); + console.log(startEvents); + + // add ProcessVariablesTask after each start event + for (let event of startEvents) { + const startEventBo = event.element; + const startEventElement = elementRegistry.get(startEventBo.id); + + const newTaskBo = getProcessContextVariablesTask( + startEventElement, + event.parent, + bpmnFactory, + modeling, + elementRegistry + ); + + // add camunda map to outputs for each entry + for (let processVariable of processContextVariables[processId]) { + addCamundaOutputMapParameter( + newTaskBo, + processVariable.name, + processVariable.map, + bpmnFactory + ); + } } + } - return { success: true }; + return { success: true }; } /** @@ -433,52 +611,69 @@ export function createProcessContextVariablesTask(processContextVariables, rootP * @param elementRegistry The elementRegistry containing the current elements of the workflow. * @return {bpmn:Task} The ProcessContextVariables task for the start event */ -function getProcessContextVariablesTask(startEventElement, parent, bpmnFactory, modeling, elementRegistry) { - - const startEventBo = startEventElement.businessObject; - - let processVariablesTaskBo; - - // check if ProcessContextVariables task already exists - if (startEventElement.outgoing[0] - && startEventElement.outgoing[0].target - && startEventElement.outgoing[0].target.businessObject.name === 'Create Process Variables [Generated]') { - processVariablesTaskBo = startEventElement.outgoing[0].target.businessObject; - - } else { - processVariablesTaskBo = bpmnFactory.create('bpmn:Task'); - processVariablesTaskBo.name = 'Create Process Variables [Generated]'; - - const outgoingFlowElements = startEventBo.outgoing || []; - - // height difference between the position of the center of a start event and a task - const Y_OFFSET_TASK = 19; - - // create new task - const newTaskElement = modeling.createShape({ - type: 'bpmn:Task', - businessObject: processVariablesTaskBo, - }, { x: startEventElement.x, y: startEventElement.y + Y_OFFSET_TASK }, parent, {}); - - modeling.updateProperties(newTaskElement, processVariablesTaskBo); - - // move start event to the left to create space for the new task - modeling.moveElements([startEventElement], { x: -120, y: 0 }); - - // connect new Task with activities which were connected with the start event - modeling.connect(startEventElement, newTaskElement, { type: 'bpmn:SequenceFlow' }); - for (let outgoingConnectionBo of outgoingFlowElements) { - const outgoingConnectionElement = elementRegistry.get(outgoingConnectionBo.id); - const target = outgoingConnectionElement.target; - - modeling.removeConnection(outgoingConnectionElement); - modeling.connect(newTaskElement, target, { - type: outgoingConnectionElement.type, - waypoints: outgoingConnectionElement.waypoints - }); - } +function getProcessContextVariablesTask( + startEventElement, + parent, + bpmnFactory, + modeling, + elementRegistry +) { + const startEventBo = startEventElement.businessObject; + + let processVariablesTaskBo; + + // check if ProcessContextVariables task already exists + if ( + startEventElement.outgoing[0] && + startEventElement.outgoing[0].target && + startEventElement.outgoing[0].target.businessObject.name === + "Create Process Variables [Generated]" + ) { + processVariablesTaskBo = + startEventElement.outgoing[0].target.businessObject; + } else { + processVariablesTaskBo = bpmnFactory.create("bpmn:Task"); + processVariablesTaskBo.name = "Create Process Variables [Generated]"; + + const outgoingFlowElements = startEventBo.outgoing || []; + + // height difference between the position of the center of a start event and a task + const Y_OFFSET_TASK = 19; + + // create new task + const newTaskElement = modeling.createShape( + { + type: "bpmn:Task", + businessObject: processVariablesTaskBo, + }, + { x: startEventElement.x, y: startEventElement.y + Y_OFFSET_TASK }, + parent, + {} + ); + + modeling.updateProperties(newTaskElement, processVariablesTaskBo); + + // move start event to the left to create space for the new task + modeling.moveElements([startEventElement], { x: -120, y: 0 }); + + // connect new Task with activities which were connected with the start event + modeling.connect(startEventElement, newTaskElement, { + type: "bpmn:SequenceFlow", + }); + for (let outgoingConnectionBo of outgoingFlowElements) { + const outgoingConnectionElement = elementRegistry.get( + outgoingConnectionBo.id + ); + const target = outgoingConnectionElement.target; + + modeling.removeConnection(outgoingConnectionElement); + modeling.connect(newTaskElement, target, { + type: outgoingConnectionElement.type, + waypoints: outgoingConnectionElement.waypoints, + }); } - return processVariablesTaskBo; + } + return processVariablesTaskBo; } /** @@ -488,34 +683,41 @@ function getProcessContextVariablesTask(startEventElement, parent, bpmnFactory, * @param elementRegistry The elementRegistry containing all elements of the current workflow * @return {boolean} */ -function isDataMapObjectUsedBeforeInitialized(dataMapObjectElement, elementRegistry) { - - // return false if the element does not have incoming and outgoing connections - if (!dataMapObjectElement.incoming - || dataMapObjectElement.incoming.length === 0 - || !dataMapObjectElement.outgoing - || dataMapObjectElement.outgoing.length === 0) { - return false; +function isDataMapObjectUsedBeforeInitialized( + dataMapObjectElement, + elementRegistry +) { + // return false if the element does not have incoming and outgoing connections + if ( + !dataMapObjectElement.incoming || + dataMapObjectElement.incoming.length === 0 || + !dataMapObjectElement.outgoing || + dataMapObjectElement.outgoing.length === 0 + ) { + return false; + } + + // if there is one outgoing that connection with a target located before the first outgoing connection, return false + for (let incomingConnection of dataMapObjectElement.incoming) { + // check if there exists at least one outgoing connection to an element that is located in the sequence flow before + // the target of the incomingConnection + for (let outgoingConnection of dataMapObjectElement.outgoing) { + const found = findSequenceFlowConnection( + outgoingConnection.target, + incomingConnection.source, + new Set(), + elementRegistry + ); + if (found) { + // there is an outgoing connection with a target before the incoming connection + break; + } + + // found one incoming connection that is located before all outgoing connections + return false; } - - // if there is one outgoing that connection with a target located before the first outgoing connection, return false - for (let incomingConnection of dataMapObjectElement.incoming) { - - // check if there exists at least one outgoing connection to an element that is located in the sequence flow before - // the target of the incomingConnection - for (let outgoingConnection of dataMapObjectElement.outgoing) { - const found = findSequenceFlowConnection(outgoingConnection.target, incomingConnection.source, new Set(), elementRegistry); - if (found) { - - // there is an outgoing connection with a target before the incoming connection - break; - } - - // found one incoming connection that is located before all outgoing connections - return false; - } - } - return true; + } + return true; } /** @@ -525,14 +727,14 @@ function isDataMapObjectUsedBeforeInitialized(dataMapObjectElement, elementRegis * @return {string} The documentation as a string. */ function createDataMapObjectDocs(dataMapObjectBo) { - let doc = '\n \n Replaced DataMapObject, represents the following data: \n'; + let doc = "\n \n Replaced DataMapObject, represents the following data: \n"; - const contentMap = {}; - for (let contentEntry of dataMapObjectBo.get(consts.CONTENT)) { - contentMap[contentEntry.name] = contentEntry.value; - } + const contentMap = {}; + for (let contentEntry of dataMapObjectBo.get(consts.CONTENT)) { + contentMap[contentEntry.name] = contentEntry.value; + } - return doc.concat(JSON.stringify(contentMap)); + return doc.concat(JSON.stringify(contentMap)); } /** @@ -542,14 +744,14 @@ function createDataMapObjectDocs(dataMapObjectBo) { * @return {string} The documentation as a string. */ function createDataStoreMapDocs(dataStoreMapBo) { - let doc = '\n \n Replaced DataStoreMap, represents the following data: \n'; + let doc = "\n \n Replaced DataStoreMap, represents the following data: \n"; - const detailsMap = {}; - for (let detailsEntry of dataStoreMapBo.get(consts.DETAILS)) { - detailsMap[detailsEntry.name] = detailsEntry.value; - } + const detailsMap = {}; + for (let detailsEntry of dataStoreMapBo.get(consts.DETAILS)) { + detailsMap[detailsEntry.name] = detailsEntry.value; + } - return doc.concat(JSON.stringify(detailsMap)); + return doc.concat(JSON.stringify(detailsMap)); } /** @@ -560,16 +762,20 @@ function createDataStoreMapDocs(dataStoreMapBo) { * @return {string} The documentation string */ function createTransformationSourceDocs(transformationAssociationElement) { - const target = transformationAssociationElement.target; + const target = transformationAssociationElement.target; - const doc = `\n \n This object was transformed into ${target.name || target.id}. The transformation was defined by the following expressions: \n`; + const doc = `\n \n This object was transformed into ${ + target.name || target.id + }. The transformation was defined by the following expressions: \n`; - const expressionsMap = {}; - for (let expression of transformationAssociationElement.businessObject.get(consts.EXPRESSIONS)) { - expressionsMap[expression.name] = expression.value; - } + const expressionsMap = {}; + for (let expression of transformationAssociationElement.businessObject.get( + consts.EXPRESSIONS + )) { + expressionsMap[expression.name] = expression.value; + } - return doc.concat(JSON.stringify(expressionsMap)); + return doc.concat(JSON.stringify(expressionsMap)); } /** @@ -580,14 +786,18 @@ function createTransformationSourceDocs(transformationAssociationElement) { * @return {string} The documentation string */ function createTransformationTargetDocs(transformationAssociationElement) { - const source = transformationAssociationElement.source; + const source = transformationAssociationElement.source; - const doc = `\n \n This object was created through a transformation of ${source.name || source.id}. The transformation was defined by the following expressions: \n`; + const doc = `\n \n This object was created through a transformation of ${ + source.name || source.id + }. The transformation was defined by the following expressions: \n`; - const expressionsMap = {}; - for (let expression of transformationAssociationElement.businessObject.get(consts.EXPRESSIONS)) { - expressionsMap[expression.name] = expression.value; - } + const expressionsMap = {}; + for (let expression of transformationAssociationElement.businessObject.get( + consts.EXPRESSIONS + )) { + expressionsMap[expression.name] = expression.value; + } - return doc.concat(JSON.stringify(expressionsMap)); -} \ No newline at end of file + return doc.concat(JSON.stringify(expressionsMap)); +} diff --git a/components/bpmn-q/modeler-component/extensions/data-extension/ui/UpdateTransformationConfigurations.js b/components/bpmn-q/modeler-component/extensions/data-extension/ui/UpdateTransformationConfigurations.js index f0acc97b..86e8ef51 100644 --- a/components/bpmn-q/modeler-component/extensions/data-extension/ui/UpdateTransformationConfigurations.js +++ b/components/bpmn-q/modeler-component/extensions/data-extension/ui/UpdateTransformationConfigurations.js @@ -1,5 +1,5 @@ -import React from 'react'; -import {updateTransformationTaskConfigurations} from "../transf-task-configs/TransformationTaskConfigurations"; +import React from "react"; +import { updateTransformationTaskConfigurations } from "../transf-task-configs/TransformationTaskConfigurations"; /** * React button component which updates the transformation task configurations when clicked. @@ -8,12 +8,18 @@ import {updateTransformationTaskConfigurations} from "../transf-task-configs/Tra * @constructor */ export default function UpdateTransformationTaskConfigurationsButton() { - - return
- -
; + return ( +
+ +
+ ); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js index 63c1d74d..02c17fc5 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/OpenTOSCAPlugin.js @@ -2,26 +2,25 @@ import React from "react"; import OpenTOSCATab from "./configTabs/OpenTOSCATab"; -import OpenTOSCAStyles from './styling/opentosca.css'; +import OpenTOSCAStyles from "./styling/opentosca.css"; import DeploymentPlugin from "./ui/deployment/services/DeploymentPlugin"; import OpenTOSCAExtensionModule from "./modeling"; -let OpenTOSCAModdleExtension = require('./resources/opentosca4bpmn.json'); - +let OpenTOSCAModdleExtension = require("./resources/opentosca4bpmn.json"); /** * Plugin Object of the OpenTOSCA extension. Used to register the plugin in the plugin handler of the modeler. */ export default { - buttons: [], - configTabs: [ - { - tabId: 'OpenTOSCAEndpointTab', - tabTitle: 'OpenTOSCA Plugin', - configTab: OpenTOSCATab, - } - ], - extensionModule: OpenTOSCAExtensionModule, - moddleDescription: OpenTOSCAModdleExtension, - name: 'opentosca', - styling: [OpenTOSCAStyles] -}; \ No newline at end of file + buttons: [], + configTabs: [ + { + tabId: "OpenTOSCAEndpointTab", + tabTitle: "OpenTOSCA Plugin", + configTab: OpenTOSCATab, + }, + ], + extensionModule: OpenTOSCAExtensionModule, + moddleDescription: OpenTOSCAModdleExtension, + name: "opentosca", + styling: [OpenTOSCAStyles], +}; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js b/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js index ca6cb92b..9b4dacab 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/configTabs/OpenTOSCATab.js @@ -1,5 +1,5 @@ -import React, {useState} from 'react'; -import {getModeler} from "../../../editor/ModelerHandler"; +import React, { useState } from "react"; +import { getModeler } from "../../../editor/ModelerHandler"; import * as config from "../framework-config/config-manager"; /** @@ -10,72 +10,79 @@ import * as config from "../framework-config/config-manager"; * @constructor */ export default function OpenTOSCATab() { + const [opentoscaEndpoint, setOpentoscaEndpoint] = useState( + config.getOpenTOSCAEndpoint() + ); + const [wineryEndpoint, setWineryEndpoint] = useState( + config.getWineryEndpoint() + ); - const [opentoscaEndpoint, setOpentoscaEndpoint] = useState(config.getOpenTOSCAEndpoint()); - const [wineryEndpoint, setWineryEndpoint] = useState(config.getWineryEndpoint()); + const modeler = getModeler(); - const modeler = getModeler(); + const editorActions = modeler.get("editorActions"); + const eventBus = modeler.get("eventBus"); - const editorActions = modeler.get('editorActions'); - const eventBus = modeler.get('eventBus'); + // register editor action listener for changes in config entries + if (!editorActions._actions.hasOwnProperty("opentoscaEndpointChanged")) { + editorActions.register({ + opentoscaEndpointChanged: function (opentoscaEndpoint) { + self.modeler.config.opentoscaEndpoint = opentoscaEndpoint; + }, + }); + } + if (!editorActions._actions.hasOwnProperty("wineryEndpointChanged")) { + editorActions.register({ + wineryEndpointChanged: function (wineryEndpoint) { + self.modeler.config.wineryEndpoint = wineryEndpoint; + eventBus.fire("config.updated", self.modeler.config); + }, + }); + } - // register editor action listener for changes in config entries - if (!editorActions._actions.hasOwnProperty('opentoscaEndpointChanged')) { - editorActions.register({ - opentoscaEndpointChanged: function (opentoscaEndpoint) { - self.modeler.config.opentoscaEndpoint = opentoscaEndpoint; - } - }); - } - if (!editorActions._actions.hasOwnProperty('wineryEndpointChanged')) { - editorActions.register({ - wineryEndpointChanged: function (wineryEndpoint) { - self.modeler.config.wineryEndpoint = wineryEndpoint; - eventBus.fire('config.updated', self.modeler.config); - } - }); - } + // save changed config entries on close + OpenTOSCATab.prototype.onClose = () => { + modeler.config.opentoscaEndpoint = opentoscaEndpoint; + modeler.config.wineryEndpoint = wineryEndpoint; + config.setOpenTOSCAEndpoint(opentoscaEndpoint); + config.setWineryEndpoint(wineryEndpoint); + }; - // save changed config entries on close - OpenTOSCATab.prototype.onClose = () => { - modeler.config.opentoscaEndpoint = opentoscaEndpoint; - modeler.config.wineryEndpoint = wineryEndpoint; - config.setOpenTOSCAEndpoint(opentoscaEndpoint); - config.setWineryEndpoint(wineryEndpoint); - }; - - return <> -

OpenTOSCA

- - - - - - - - - - - -
OpenTOSCA Endpoint: - setOpentoscaEndpoint(event.target.value)}/> -
Winery Endpoint: - setWineryEndpoint(event.target.value)}/> -
- ; + return ( + <> +

OpenTOSCA

+ + + + + + + + + + + +
OpenTOSCA Endpoint: + setOpentoscaEndpoint(event.target.value)} + /> +
Winery Endpoint: + setWineryEndpoint(event.target.value)} + /> +
+ + ); } OpenTOSCATab.prototype.config = () => { - const modeler = getModeler(); + const modeler = getModeler(); - modeler.config.opentoscaEndpoint = config.getOpenTOSCAEndpoint(); - modeler.config.wineryEndpoint = config.getWineryEndpoint(); -}; \ No newline at end of file + modeler.config.opentoscaEndpoint = config.getOpenTOSCAEndpoint(); + modeler.config.wineryEndpoint = config.getWineryEndpoint(); +}; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js index 0cd9737e..38b40e9d 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/BindingUtils.js @@ -10,8 +10,12 @@ */ import * as config from "../framework-config/config-manager"; -const QUANTME_NAMESPACE_PULL_ENCODED = encodeURIComponent(encodeURIComponent('http://quantil.org/quantme/pull')); -const QUANTME_NAMESPACE_PUSH_ENCODED = encodeURIComponent(encodeURIComponent('http://quantil.org/quantme/push')); +const QUANTME_NAMESPACE_PULL_ENCODED = encodeURIComponent( + encodeURIComponent("http://quantil.org/quantme/pull") +); +const QUANTME_NAMESPACE_PUSH_ENCODED = encodeURIComponent( + encodeURIComponent("http://quantil.org/quantme/push") +); /** * Check whether the given ServiceTask has an attached deployment model that should be bound using pull or push mode @@ -22,22 +26,25 @@ const QUANTME_NAMESPACE_PUSH_ENCODED = encodeURIComponent(encodeURIComponent('ht * or undefined if unable to determine pull or push */ export function getBindingType(serviceTask) { - let urlSplit = serviceTask.deploymentModelUrl.split('servicetemplates/'); - if (urlSplit.length !== 2) { - console.warn('Deployment model url is invalid: %s', serviceTask.deploymentModelUrl); - return undefined; - } - let namespace = urlSplit[1]; + let urlSplit = serviceTask.deploymentModelUrl.split("servicetemplates/"); + if (urlSplit.length !== 2) { + console.warn( + "Deployment model url is invalid: %s", + serviceTask.deploymentModelUrl + ); + return undefined; + } + let namespace = urlSplit[1]; - if (namespace.startsWith(QUANTME_NAMESPACE_PUSH_ENCODED)) { - return 'push'; - } + if (namespace.startsWith(QUANTME_NAMESPACE_PUSH_ENCODED)) { + return "push"; + } - if (namespace.startsWith(QUANTME_NAMESPACE_PULL_ENCODED)) { - return 'pull'; - } + if (namespace.startsWith(QUANTME_NAMESPACE_PULL_ENCODED)) { + return "pull"; + } - return undefined; + return undefined; } /** @@ -50,33 +57,47 @@ export function getBindingType(serviceTask) { * @return {{success: boolean}} true if binding is successful, false otherwise */ export function bindUsingPull(csar, serviceTaskId, elementRegistry, modeling) { - - if (csar.topicName === undefined || serviceTaskId === undefined || elementRegistry === undefined || modeling === undefined) { - console.error('Topic name, service task id, element registry, and modeling required for binding using pull!'); - return { success: false }; - } - - // retrieve service task to bind - let serviceTask = elementRegistry.get(serviceTaskId); - if (serviceTask === undefined) { - console.error('Unable to retrieve corresponding task for id: %s', serviceTaskId); - return { success: false }; - } - - let deploymentModelUrl = serviceTask.businessObject.get('opentosca:deploymentModelUrl'); - if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { - deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); - } - - // remove deployment model URL and set topic - - modeling.updateProperties(serviceTask, { - 'opentosca:deploymentModelUrl': deploymentModelUrl, - 'opentosca:deploymentBuildPlanInstanceUrl': csar.buildPlanUrl, - type: 'external', - topic: csar.topicName - }); - return {success: true}; + if ( + csar.topicName === undefined || + serviceTaskId === undefined || + elementRegistry === undefined || + modeling === undefined + ) { + console.error( + "Topic name, service task id, element registry, and modeling required for binding using pull!" + ); + return { success: false }; + } + + // retrieve service task to bind + let serviceTask = elementRegistry.get(serviceTaskId); + if (serviceTask === undefined) { + console.error( + "Unable to retrieve corresponding task for id: %s", + serviceTaskId + ); + return { success: false }; + } + + let deploymentModelUrl = serviceTask.businessObject.get( + "opentosca:deploymentModelUrl" + ); + if (deploymentModelUrl.startsWith("{{ wineryEndpoint }}")) { + deploymentModelUrl = deploymentModelUrl.replace( + "{{ wineryEndpoint }}", + config.getWineryEndpoint() + ); + } + + // remove deployment model URL and set topic + + modeling.updateProperties(serviceTask, { + "opentosca:deploymentModelUrl": deploymentModelUrl, + "opentosca:deploymentBuildPlanInstanceUrl": csar.buildPlanUrl, + type: "external", + topic: csar.topicName, + }); + return { success: true }; } /** @@ -87,54 +108,61 @@ export function bindUsingPull(csar, serviceTaskId, elementRegistry, modeling) { * @param elementRegistry the element registry of the modeler to find workflow elements * @return {{success: boolean}} true if binding is successful, false otherwise */ -export async function bindUsingPush(csar, serviceTaskId, elementRegistry, modeling) { - let url = await extractSelfserviceApplicationUrl(csar.properties); - let success = false; - - if (csar === undefined || serviceTaskId === undefined || elementRegistry === undefined) { - console.error('CSAR details, service task id, and element registry required for binding using push!'); - return { success: false }; - } - - // retrieve service task to bind - let serviceTask = elementRegistry.get(serviceTaskId); - if (serviceTask === undefined) { - console.error('Unable to retrieve corresponding task for id: %s', serviceTaskId); - return { success: false }; - } - - let extensionElements = serviceTask.businessObject.extensionElements.values; - for (let i = 0; i < extensionElements.length; i++) { - let extensionElement = extensionElements[i]; - if (extensionElement.$type === 'camunda:Connector') { - let inputParameters = extensionElement.inputOutput.inputParameters; - for (let j = 0; j < inputParameters.length; j++) { - let inputParameter = inputParameters[j]; - if (inputParameter.name === 'url') { - let connectorUrl = serviceTask.businessObject.connectorUrl; - inputParameter.value = url + connectorUrl; - success = true; - } - } +export async function bindUsingPush(csar, serviceTaskId, elementRegistry) { + let url = await extractSelfserviceApplicationUrl(csar.properties); + let success = false; + + if ( + csar === undefined || + serviceTaskId === undefined || + elementRegistry === undefined + ) { + console.error( + "CSAR details, service task id, and element registry required for binding using push!" + ); + return { success: false }; + } + + // retrieve service task to bind + let serviceTask = elementRegistry.get(serviceTaskId); + if (serviceTask === undefined) { + console.error( + "Unable to retrieve corresponding task for id: %s", + serviceTaskId + ); + return { success: false }; + } + + let extensionElements = serviceTask.businessObject.extensionElements.values; + for (let i = 0; i < extensionElements.length; i++) { + let extensionElement = extensionElements[i]; + if (extensionElement.$type === "camunda:Connector") { + let inputParameters = extensionElement.inputOutput.inputParameters; + for (let j = 0; j < inputParameters.length; j++) { + let inputParameter = inputParameters[j]; + if (inputParameter.name === "url") { + let connectorUrl = serviceTask.businessObject.connectorUrl; + inputParameter.value = url + connectorUrl; + success = true; } + } } - return { success: success }; + } + return { success: success }; } -async function extractSelfserviceApplicationUrl(propertiesUrl, csar) { - - let properties = await fetchProperties(propertiesUrl); - let json = JSON.parse(properties); - const value = json.selfserviceApplicationUrl; - return value; +async function extractSelfserviceApplicationUrl(propertiesUrl) { + let properties = await fetchProperties(propertiesUrl); + let json = JSON.parse(properties); + return json.selfserviceApplicationUrl; } async function fetchProperties(url) { - const response = await fetch(url, { - headers: { - Accept: "application/json" - } - }); - const json = await response.text(); - return json; + const response = await fetch(url, { + headers: { + Accept: "application/json", + }, + }); + const json = await response.text(); + return json; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js index 018e2670..cbb527ce 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/DeploymentUtils.js @@ -9,8 +9,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {getBindingType} from './BindingUtils'; -import {getFlowElementsRecursively} from '../../../editor/util/ModellingUtilities'; +import { getBindingType } from "./BindingUtils"; +import { getFlowElementsRecursively } from "../../../editor/util/ModellingUtilities"; /** * Get the ServiceTasks of the current workflow that have an attached deployment model to deploy the corresponding service starting from the given root element @@ -19,39 +19,40 @@ import {getFlowElementsRecursively} from '../../../editor/util/ModellingUtilitie * @return the list of ServiceTasks with attached deployment models to deploy the required services */ export function getServiceTasksToDeploy(startElement) { - let csarsToDeploy = []; + let csarsToDeploy = []; - if (startElement === undefined) { - console.warn('Element to start is undefined!'); - return csarsToDeploy; - } + if (startElement === undefined) { + console.warn("Element to start is undefined!"); + return csarsToDeploy; + } - // search for service tasks with assigned deployment model - let flowElements = getFlowElementsRecursively(startElement); - for (let i = 0; i < flowElements.length; i++) { - let flowElement = flowElements[i]; + // search for service tasks with assigned deployment model + let flowElements = getFlowElementsRecursively(startElement); + for (let i = 0; i < flowElements.length; i++) { + let flowElement = flowElements[i]; - if (isDeployableServiceTask(flowElement)) { - console.log('Found deployable service task: ', flowElement); + if (isDeployableServiceTask(flowElement)) { + console.log("Found deployable service task: ", flowElement); - // check if CSAR was already added for another service task - let csarEntry = csarsToDeploy.find(serviceTask => flowElement.deploymentModelUrl === serviceTask.url); - if (csarEntry !== undefined) { - console.log('Adding to existing CSAR entry...'); - csarEntry.serviceTaskIds.push(flowElement.id); - } else { - csarsToDeploy.push( - { - serviceTaskIds: [flowElement.id], - url: flowElement.deploymentModelUrl, - type: getBindingType(flowElement), - csarName: getCSARName(flowElement) - }); - } - } + // check if CSAR was already added for another service task + let csarEntry = csarsToDeploy.find( + (serviceTask) => flowElement.deploymentModelUrl === serviceTask.url + ); + if (csarEntry !== undefined) { + console.log("Adding to existing CSAR entry..."); + csarEntry.serviceTaskIds.push(flowElement.id); + } else { + csarsToDeploy.push({ + serviceTaskIds: [flowElement.id], + url: flowElement.deploymentModelUrl, + type: getBindingType(flowElement), + csarName: getCSARName(flowElement), + }); + } } + } - return csarsToDeploy; + return csarsToDeploy; } /** @@ -61,9 +62,9 @@ export function getServiceTasksToDeploy(startElement) { * @return {*} the CSAR name */ function getCSARName(serviceTask) { - let url = serviceTask.deploymentModelUrl.split('/?csar')[0]; - let urlSplit = url.split('/'); - return urlSplit[urlSplit.length - 1] + '.csar'; + let url = serviceTask.deploymentModelUrl.split("/?csar")[0]; + let urlSplit = url.split("/"); + return urlSplit[urlSplit.length - 1] + ".csar"; } /** @@ -73,5 +74,10 @@ function getCSARName(serviceTask) { * @return {*|boolean} true if the element is a ServiceTask and has an assigned deployment model, false otherwise */ export function isDeployableServiceTask(element) { - return element.$type && element.$type === 'bpmn:ServiceTask' && element.deploymentModelUrl && getBindingType(element) !== undefined; + return ( + element.$type && + element.$type === "bpmn:ServiceTask" && + element.deploymentModelUrl && + getBindingType(element) !== undefined + ); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js index 1804914e..be7e0136 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/OpenTOSCAUtils.js @@ -9,7 +9,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {fetch} from 'whatwg-fetch'; +import { fetch } from "whatwg-fetch"; /** * Upload the CSAR located at the given URL to the connected OpenTOSCA Container and return the corresponding URL and required input parameters @@ -19,56 +19,67 @@ import {fetch} from 'whatwg-fetch'; * @param url the URL pointing to the CSAR * @param wineryEndpoint the endpoint of the Winery containing the CSAR to upload */ -export async function uploadCSARToContainer(opentoscaEndpoint, csarName, url, wineryEndpoint) { - - if (opentoscaEndpoint === undefined) { - console.error('OpenTOSCA endpoint is undefined. Unable to upload CSARs...'); - return {success: false}; +export async function uploadCSARToContainer( + opentoscaEndpoint, + csarName, + url, + wineryEndpoint +) { + if (opentoscaEndpoint === undefined) { + console.error("OpenTOSCA endpoint is undefined. Unable to upload CSARs..."); + return { success: false }; + } + + try { + if (url.startsWith("{{ wineryEndpoint }}")) { + url = url.replace("{{ wineryEndpoint }}", wineryEndpoint); + } + console.log( + "Checking if CSAR at following URL is already uploaded to the OpenTOSCA Container: ", + url + ); + + // check if CSAR is already uploaded + let getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); + + if (!getCSARResult.success) { + console.log("CSAR is not yet uploaded. Uploading..."); + + let body = { + enrich: "false", + name: csarName, + url: url, + }; + + // upload the CSAR + await fetch(opentoscaEndpoint, { + method: "POST", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }); + + // check successful upload and retrieve corresponding url + getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); } - try { - if (url.startsWith('{{ wineryEndpoint }}')) { - url = url.replace('{{ wineryEndpoint }}', wineryEndpoint); - } - console.log('Checking if CSAR at following URL is already uploaded to the OpenTOSCA Container: ', url); - - // check if CSAR is already uploaded - let getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); - - if (!getCSARResult.success) { - console.log('CSAR is not yet uploaded. Uploading...'); - - let body = { - enrich: 'false', - name: csarName, - url: url - }; - - // upload the CSAR - await fetch(opentoscaEndpoint, { - method: 'POST', - body: JSON.stringify(body), - headers: {'Content-Type': 'application/json'} - }); - - // check successful upload and retrieve corresponding url - getCSARResult = await getBuildPlanForCSAR(opentoscaEndpoint, csarName); - } - - if (!getCSARResult.success) { - console.error('Uploading CSAR failed!'); - return {success: false}; - } - - // retrieve input parameters for the build plan - let buildPlanResult = await fetch(getCSARResult.url); - let buildPlanResultJson = await buildPlanResult.json(); - - return {success: true, url: getCSARResult.url, inputParameters: buildPlanResultJson.input_parameters}; - } catch (e) { - console.error('Error while uploading CSAR: ' + e); - return {success: false}; + if (!getCSARResult.success) { + console.error("Uploading CSAR failed!"); + return { success: false }; } + + // retrieve input parameters for the build plan + let buildPlanResult = await fetch(getCSARResult.url); + let buildPlanResultJson = await buildPlanResult.json(); + + return { + success: true, + url: getCSARResult.url, + inputParameters: buildPlanResultJson.input_parameters, + }; + } catch (e) { + console.error("Error while uploading CSAR: " + e); + return { success: false }; + } } /** @@ -79,31 +90,29 @@ export async function uploadCSARToContainer(opentoscaEndpoint, csarName, url, wi * @return the status whether the given CSAR is uploaded and the corresponding build plan link if available */ async function getBuildPlanForCSAR(opentoscaEndpoint, csarName) { - - // get all currently deployed CSARs - let response = await fetch(opentoscaEndpoint); - let responseJson = await response.json(); - - let deployedCSARs = responseJson.csars; - if (deployedCSARs === undefined) { - - // no CSARs available - return {success: false}; - } - - for (let i = 0; i < deployedCSARs.length; i++) { - let deployedCSAR = deployedCSARs[i]; - if (deployedCSAR.id === csarName) { - console.log('Found uploaded CSAR with id: %s', csarName); - let url = deployedCSAR._links.self.href; - - // retrieve the URl to the build plan required to get the input parameters and to instantiate the CSAR - return getBuildPlanUrl(url); - } + // get all currently deployed CSARs + let response = await fetch(opentoscaEndpoint); + let responseJson = await response.json(); + + let deployedCSARs = responseJson.csars; + if (deployedCSARs === undefined) { + // no CSARs available + return { success: false }; + } + + for (let i = 0; i < deployedCSARs.length; i++) { + let deployedCSAR = deployedCSARs[i]; + if (deployedCSAR.id === csarName) { + console.log("Found uploaded CSAR with id: %s", csarName); + let url = deployedCSAR._links.self.href; + + // retrieve the URl to the build plan required to get the input parameters and to instantiate the CSAR + return getBuildPlanUrl(url); } + } - // unable to find CSAR - return {success: false}; + // unable to find CSAR + return { success: false }; } /** @@ -113,25 +122,31 @@ async function getBuildPlanForCSAR(opentoscaEndpoint, csarName) { * @return the URL to the build plan for the given CSAR */ async function getBuildPlanUrl(csarUrl) { - - let response = await fetch(csarUrl + '/servicetemplates'); - let responseJson = await response.json(); - - if (!responseJson.service_templates || responseJson.service_templates.length !== 1) { - console.error('Unable to find service template in CSAR at URL: %s', csarUrl); - return {success: false}; - } - - let buildPlansUrl = responseJson.service_templates[0]._links.self.href + '/buildplans'; - response = await fetch(buildPlansUrl); - responseJson = await response.json(); - - if (!responseJson.plans || responseJson.plans.length !== 1) { - console.error('Unable to find build plan at URL: %s', buildPlansUrl); - return {success: false}; - } - - return {success: true, url: responseJson.plans[0]._links.self.href}; + let response = await fetch(csarUrl + "/servicetemplates"); + let responseJson = await response.json(); + + if ( + !responseJson.service_templates || + responseJson.service_templates.length !== 1 + ) { + console.error( + "Unable to find service template in CSAR at URL: %s", + csarUrl + ); + return { success: false }; + } + + let buildPlansUrl = + responseJson.service_templates[0]._links.self.href + "/buildplans"; + response = await fetch(buildPlansUrl); + responseJson = await response.json(); + + if (!responseJson.plans || responseJson.plans.length !== 1) { + console.error("Unable to find build plan at URL: %s", buildPlansUrl); + return { success: false }; + } + + return { success: true, url: responseJson.plans[0]._links.self.href }; } /** @@ -142,97 +157,108 @@ async function getBuildPlanUrl(csarUrl) { * @return the result of the instance creation (success, endpoint, topic on which the service listens, ...) */ export async function createServiceInstance(csar, camundaEngineEndpoint) { - - let result = {success: false}; - - let inputParameters = csar.inputParameters; - if (csar.type === 'pull') { - - // get special parameters that are required to bind services using external tasks / the pulling pattern - let camundaTopicParam = inputParameters.find((param) => param.name === 'camundaTopic'); - let camundaEndpointParam = inputParameters.find((param) => param.name === 'camundaEndpoint'); - - // abort if parameters are not available - if (camundaTopicParam === undefined || camundaEndpointParam === undefined) { - console.error('Unable to pass topic to poll to service instance creation. Service binding will fail!'); - return result; - } - - // generate topic for the binding - let topicName = makeId(12); - - camundaTopicParam.value = topicName; - camundaEndpointParam.value = camundaEngineEndpoint; - result.topicName = topicName; - } - - // trigger instance creation - let instanceCreationResponse = await fetch(csar.buildPlanUrl + '/instances', { - method: 'POST', - body: JSON.stringify(inputParameters), - headers: {'Content-Type': 'application/json'} - }); - let instanceCreationResponseJson = await instanceCreationResponse.json(); - - // wait for the service instance to be created - await new Promise(r => setTimeout(r, 5000)); - - // get service template instance to poll for completness - let buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - let buildPlanResponseJson = await buildPlanResponse.json(); - - // retry polling 10 times, creation of the build time takes some time - for (let retry = 0; retry < 10; retry++) { - - // stop retries in case of correct response - if (buildPlanResponseJson._links) { - break; - } - - await new Promise(r => setTimeout(r, 5000)); - - console.log('Retry fetching build plan'); - - buildPlanResponse = await fetch(csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - buildPlanResponseJson = await buildPlanResponse.json(); + let result = { success: false }; + + let inputParameters = csar.inputParameters; + if (csar.type === "pull") { + // get special parameters that are required to bind services using external tasks / the pulling pattern + let camundaTopicParam = inputParameters.find( + (param) => param.name === "camundaTopic" + ); + let camundaEndpointParam = inputParameters.find( + (param) => param.name === "camundaEndpoint" + ); + + // abort if parameters are not available + if (camundaTopicParam === undefined || camundaEndpointParam === undefined) { + console.error( + "Unable to pass topic to poll to service instance creation. Service binding will fail!" + ); + return result; } - if (!buildPlanResponseJson._links) { - console.log('Unable to fetch build plans for ' + csar.buildPlanUrl + '/instances/' + instanceCreationResponseJson); - result.success = false; - return result; + // generate topic for the binding + let topicName = makeId(12); + + camundaTopicParam.value = topicName; + camundaEndpointParam.value = camundaEngineEndpoint; + result.topicName = topicName; + } + + // trigger instance creation + let instanceCreationResponse = await fetch(csar.buildPlanUrl + "/instances", { + method: "POST", + body: JSON.stringify(inputParameters), + headers: { "Content-Type": "application/json" }, + }); + let instanceCreationResponseJson = await instanceCreationResponse.json(); + + // wait for the service instance to be created + await new Promise((r) => setTimeout(r, 5000)); + + // get service template instance to poll for completness + let buildPlanResponse = await fetch( + csar.buildPlanUrl + "/instances/" + instanceCreationResponseJson + ); + let buildPlanResponseJson = await buildPlanResponse.json(); + + // retry polling 10 times, creation of the build time takes some time + for (let retry = 0; retry < 10; retry++) { + // stop retries in case of correct response + if (buildPlanResponseJson._links) { + break; } - let pollingUrl = buildPlanResponseJson._links.service_template_instance.href; + await new Promise((r) => setTimeout(r, 5000)); - let state = 'CREATING'; - console.log('Polling for finished service instance at URL: %s', pollingUrl); - let properties = ''; - while (!(state === 'CREATED' || state === 'FAILED')) { + console.log("Retry fetching build plan"); - // wait 5 seconds for next poll - await new Promise(r => setTimeout(r, 5000)); - // poll for current state - let pollingResponse = await fetch(pollingUrl); - let pollingResponseJson = await pollingResponse.json(); - console.log('Polling response: ', pollingResponseJson); - properties = pollingResponseJson._links.properties.href; + buildPlanResponse = await fetch( + csar.buildPlanUrl + "/instances/" + instanceCreationResponseJson + ); + buildPlanResponseJson = await buildPlanResponse.json(); + } - state = pollingResponseJson.state; - } - - result.success = true; - result.properties = properties; + if (!buildPlanResponseJson._links) { + console.log( + "Unable to fetch build plans for " + + csar.buildPlanUrl + + "/instances/" + + instanceCreationResponseJson + ); + result.success = false; return result; + } + + let pollingUrl = buildPlanResponseJson._links.service_template_instance.href; + + let state = "CREATING"; + console.log("Polling for finished service instance at URL: %s", pollingUrl); + let properties = ""; + while (!(state === "CREATED" || state === "FAILED")) { + // wait 5 seconds for next poll + await new Promise((r) => setTimeout(r, 5000)); + // poll for current state + let pollingResponse = await fetch(pollingUrl); + let pollingResponseJson = await pollingResponse.json(); + console.log("Polling response: ", pollingResponseJson); + properties = pollingResponseJson._links.properties.href; + + state = pollingResponseJson.state; + } + + result.success = true; + result.properties = properties; + return result; } - export function makeId(length) { - let result = ''; - let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; + let result = ""; + let characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js index 0072e717..3dbd3b1e 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/deployment/WineryUtils.js @@ -10,290 +10,391 @@ */ import * as config from "../framework-config/config-manager"; -import {getWineryEndpoint} from "../framework-config/config-manager"; -import {fetch} from "whatwg-fetch"; +import { getWineryEndpoint } from "../framework-config/config-manager"; +import { fetch } from "whatwg-fetch"; -const QUANTME_NAMESPACE_PUSH = 'http://quantil.org/quantme/push'; +const QUANTME_NAMESPACE_PUSH = "http://quantil.org/quantme/push"; export async function createArtifactTemplate(name, artifactTypeQName) { - const artifactTemplate = { - localname: name, - namespace: QUANTME_NAMESPACE_PUSH + '/artifacttemplates', - type: artifactTypeQName, - }; - const response = await fetch(`${getWineryEndpoint()}/artifacttemplates`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'text/plain' - }, - body: JSON.stringify(artifactTemplate) - }); - return response.text(); + const artifactTemplate = { + localname: name, + namespace: QUANTME_NAMESPACE_PUSH + "/artifacttemplates", + type: artifactTypeQName, + }; + const response = await fetch(`${getWineryEndpoint()}/artifacttemplates`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/plain", + }, + body: JSON.stringify(artifactTemplate), + }); + return response.text(); } export async function addFileToArtifactTemplate(artifactTemplateAddress, file) { - const formData = new FormData(); - formData.append('file', file); - const response = await fetch(`${getWineryEndpoint()}/artifacttemplates/${artifactTemplateAddress}files`, { - method: 'POST', - body: formData, - headers: { - 'Accept': '*/*' - }, - }); - return response.json(); + const formData = new FormData(); + formData.append("file", file); + const response = await fetch( + `${getWineryEndpoint()}/artifacttemplates/${artifactTemplateAddress}files`, + { + method: "POST", + body: formData, + headers: { + Accept: "*/*", + }, + } + ); + return response.json(); } export async function createArtifactTemplateWithFile(name, artifactType, file) { - const artifactTemplateAddress = await createArtifactTemplate(name, artifactType); - await addFileToArtifactTemplate(artifactTemplateAddress, file); - return artifactTemplateAddress; + const artifactTemplateAddress = await createArtifactTemplate( + name, + artifactType + ); + await addFileToArtifactTemplate(artifactTemplateAddress, file); + return artifactTemplateAddress; } export async function createServiceTemplate(name) { - const serviceTemplate = { - localname: name, - namespace: QUANTME_NAMESPACE_PUSH, - }; - const response = await fetch(getWineryEndpoint() + '/servicetemplates', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'text/plain' - }, - body: JSON.stringify(serviceTemplate) - }); - return response.text(); + const serviceTemplate = { + localname: name, + namespace: QUANTME_NAMESPACE_PUSH, + }; + const response = await fetch(getWineryEndpoint() + "/servicetemplates", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/plain", + }, + body: JSON.stringify(serviceTemplate), + }); + return response.text(); } export async function deleteArtifactTemplate(artifactTemplateName) { - // /artifacttemplates/http%253A%252F%252Fquantil.org%252Fquantme%252Fpush%252Fartifacttemplates/ArtifactTemplate-Activity_01b3qkz/ - const response = await fetch(`${getWineryEndpoint()}/artifacttemplates/${encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH + '/'))}artifacttemplates/${encodeURIComponent(encodeURIComponent(artifactTemplateName))}`, { - method: 'DELETE', - headers: { - 'Accept': 'application/json' - }, - }); - return response.status === 204; + // /artifacttemplates/http%253A%252F%252Fquantil.org%252Fquantme%252Fpush%252Fartifacttemplates/ArtifactTemplate-Activity_01b3qkz/ + const response = await fetch( + `${getWineryEndpoint()}/artifacttemplates/${encodeURIComponent( + encodeURIComponent(QUANTME_NAMESPACE_PUSH + "/") + )}artifacttemplates/${encodeURIComponent( + encodeURIComponent(artifactTemplateName) + )}`, + { + method: "DELETE", + headers: { + Accept: "application/json", + }, + } + ); + return response.status === 204; } export async function serviceTemplateExists(serviceTemplateName) { - const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH))}/${encodeURIComponent(encodeURIComponent(serviceTemplateName))}`, { - method: 'GET', - }); - return response.status === 200; + const response = await fetch( + `${getWineryEndpoint()}/servicetemplates/${encodeURIComponent( + encodeURIComponent(QUANTME_NAMESPACE_PUSH) + )}/${encodeURIComponent(encodeURIComponent(serviceTemplateName))}`, + { + method: "GET", + } + ); + return response.status === 200; } -export async function addNodeWithArtifactToServiceTemplateByName(serviceTemplateName, nodeTypeQName, name, artifactTemplateQName, artifactName, artifactTypeQName) { - const serviceTemplateAddress = encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH)) + '/' + encodeURIComponent(encodeURIComponent(serviceTemplateName)) + '/'; - await addNodeWithArtifactToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name, artifactTemplateQName, artifactName, artifactTypeQName); - return serviceTemplateAddress; +export async function addNodeWithArtifactToServiceTemplateByName( + serviceTemplateName, + nodeTypeQName, + name, + artifactTemplateQName, + artifactName, + artifactTypeQName +) { + const serviceTemplateAddress = + encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PUSH)) + + "/" + + encodeURIComponent(encodeURIComponent(serviceTemplateName)) + + "/"; + await addNodeWithArtifactToServiceTemplate( + serviceTemplateAddress, + nodeTypeQName, + name, + artifactTemplateQName, + artifactName, + artifactTypeQName + ); + return serviceTemplateAddress; } -export async function addNodeToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name) { - const nodeTemplate = { - "documentation": [], - "any": [], - "otherAttributes": {}, - "relationshipTemplates": [], - "nodeTemplates": [ - { - "documentation": [], - "any": [], - "otherAttributes": { - "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": 1245, - "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": 350 - }, - "properties": { - "propertyType": "KV", - "kvproperties": { - "Port": "", - "Name": "" - }, - "elementName": "properties", - "namespace": "http://opentosca.org/nodetypes/propertiesdefinition/winery" - }, - "id": nodeTypeQName.split(/}(.*)/s)[1], - "type": nodeTypeQName, - "name": name, - "minInstances": 1, - "maxInstances": 1, - "x": 1245, - "y": 350, - "capabilities": [], - "requirements": [], - "deploymentArtifacts": null, - "policies": null - } - ] - }; - const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}topologytemplate`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Accept': '*/*' +export async function addNodeToServiceTemplate( + serviceTemplateAddress, + nodeTypeQName, + name +) { + const nodeTemplate = { + documentation: [], + any: [], + otherAttributes: {}, + relationshipTemplates: [], + nodeTemplates: [ + { + documentation: [], + any: [], + otherAttributes: { + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": 1245, + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": 350, }, - body: JSON.stringify(nodeTemplate) - }); - return response.status === 204; -} - -export async function addNodeWithArtifactToServiceTemplate(serviceTemplateAddress, nodeTypeQName, name, artifactTemplateQName, artifactName, artifactTypeQName) { - const nodeTemplate = { - "documentation": [], - "any": [], - "otherAttributes": {}, - "relationshipTemplates": [], - "nodeTemplates": [{ - "documentation": [], - "any": [], - "otherAttributes": { - "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": 1245, - "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": 350 - }, - "properties": { - "propertyType": "KV", - "kvproperties": { - "Port": "", - "Name": "" - }, - "elementName": "properties", - "namespace": "http://opentosca.org/nodetypes/propertiesdefinition/winery" - }, - "id": nodeTypeQName.split(/}(.*)/s)[1], - "type": nodeTypeQName, - "name": name, - "minInstances": 1, - "maxInstances": 1, - "x": 1245, - "y": 350, - "capabilities": [], - "requirements": [], - "deploymentArtifacts": [{ - "documentation": [], - "any": [], - "otherAttributes": {}, - "name": artifactName, - "artifactType": artifactTypeQName, - "artifactRef": artifactTemplateQName - }], - "policies": null - } - ] - }; - const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}topologytemplate`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Accept': '*/*' + properties: { + propertyType: "KV", + kvproperties: { + Port: "", + Name: "", + }, + elementName: "properties", + namespace: + "http://opentosca.org/nodetypes/propertiesdefinition/winery", }, - body: JSON.stringify(nodeTemplate) - }); - return response.status === 204; + id: nodeTypeQName.split(/}(.*)/s)[1], + type: nodeTypeQName, + name: name, + minInstances: 1, + maxInstances: 1, + x: 1245, + y: 350, + capabilities: [], + requirements: [], + deploymentArtifacts: null, + policies: null, + }, + ], + }; + const response = await fetch( + `${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}topologytemplate`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "*/*", + }, + body: JSON.stringify(nodeTemplate), + } + ); + return response.status === 204; } -export async function getServiceTemplateXML(serviceTemplateAddress) { - const response = await fetch(`${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}xml`, { - method: 'GET', - headers: { - 'Accept': 'application/xml' +export async function addNodeWithArtifactToServiceTemplate( + serviceTemplateAddress, + nodeTypeQName, + name, + artifactTemplateQName, + artifactName, + artifactTypeQName +) { + const nodeTemplate = { + documentation: [], + any: [], + otherAttributes: {}, + relationshipTemplates: [], + nodeTemplates: [ + { + documentation: [], + any: [], + otherAttributes: { + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}x": 1245, + "{http://www.opentosca.org/winery/extensions/tosca/2013/02/12}y": 350, + }, + properties: { + propertyType: "KV", + kvproperties: { + Port: "", + Name: "", + }, + elementName: "properties", + namespace: + "http://opentosca.org/nodetypes/propertiesdefinition/winery", }, - }); - return response.json(); + id: nodeTypeQName.split(/}(.*)/s)[1], + type: nodeTypeQName, + name: name, + minInstances: 1, + maxInstances: 1, + x: 1245, + y: 350, + capabilities: [], + requirements: [], + deploymentArtifacts: [ + { + documentation: [], + any: [], + otherAttributes: {}, + name: artifactName, + artifactType: artifactTypeQName, + artifactRef: artifactTemplateQName, + }, + ], + policies: null, + }, + ], + }; + const response = await fetch( + `${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}topologytemplate`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "*/*", + }, + body: JSON.stringify(nodeTemplate), + } + ); + return response.status === 204; } -export async function setServiceTemplateXML(serviceTemplateAddress, newXml) { - const response = await fetch(getWineryEndpoint() + '/servicetemplates/' + serviceTemplateAddress + 'xml', { - method: 'PUT', - body: newXml, - headers: {'Content-Type': 'application/xml'} - }); +export async function getServiceTemplateXML(serviceTemplateAddress) { + const response = await fetch( + `${getWineryEndpoint()}/servicetemplates/${serviceTemplateAddress}xml`, + { + method: "GET", + headers: { + Accept: "application/xml", + }, + } + ); + return response.json(); } export async function insertTopNodeTag(serviceTemplateAddress, nodeTypeQName) { - const tag = { - name: "top-node", - value: nodeTypeQName.split(/}(.*)/s)[1], - }; - const response = await fetch(getWineryEndpoint() + '/servicetemplates/' + serviceTemplateAddress + "tags/", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': '*/*' - }, - body: JSON.stringify(tag) - }); - return response.text(); + const tag = { + name: "top-node", + value: nodeTypeQName.split(/}(.*)/s)[1], + }; + const response = await fetch( + getWineryEndpoint() + + "/servicetemplates/" + + serviceTemplateAddress + + "tags/", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "*/*", + }, + body: JSON.stringify(tag), + } + ); + return response.text(); } -export async function createServiceTemplateWithNodeAndArtifact(name, nodeTypeQName, nodeName, artifactTemplateQName, artifactName, artifactTypeQName) { - const serviceTemplateAddress = await createServiceTemplate(name); - await addNodeWithArtifactToServiceTemplate(serviceTemplateAddress, nodeTypeQName, nodeName, artifactTemplateQName, artifactName, artifactTypeQName); - return serviceTemplateAddress; +export async function createServiceTemplateWithNodeAndArtifact( + name, + nodeTypeQName, + nodeName, + artifactTemplateQName, + artifactName, + artifactTypeQName +) { + const serviceTemplateAddress = await createServiceTemplate(name); + await addNodeWithArtifactToServiceTemplate( + serviceTemplateAddress, + nodeTypeQName, + nodeName, + artifactTemplateQName, + artifactName, + artifactTypeQName + ); + return serviceTemplateAddress; } export async function getArtifactTemplateInfo(artifactTemplateAddress) { - const response = await fetch(`${getWineryEndpoint()}/artifacttemplates/${artifactTemplateAddress}`, { - method: 'GET', - headers: { - 'Accept': 'application/json' - } - }); - return response.json(); + const response = await fetch( + `${getWineryEndpoint()}/artifacttemplates/${artifactTemplateAddress}`, + { + method: "GET", + headers: { + Accept: "application/json", + }, + } + ); + return response.json(); } const nodeTypeQNameMapping = new Map([ - ['{http://opentosca.org/artifacttypes}WAR', '{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1'], - ['{http://opentosca.org/artifacttypes}WAR17', '{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1'], - ['{http://opentosca.org/artifacttypes}WAR8', '{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1'], - ['{http://opentosca.org/artifacttypes}PythonArchiveArtifact', '{http://opentosca.org/nodetypes}PythonApp_3-w1'], + [ + "{http://opentosca.org/artifacttypes}WAR", + "{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1", + ], + [ + "{http://opentosca.org/artifacttypes}WAR17", + "{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1", + ], + [ + "{http://opentosca.org/artifacttypes}WAR8", + "{http://opentosca.org/nodetypes}TomcatApplication_WAR-w1", + ], + [ + "{http://opentosca.org/artifacttypes}PythonArchiveArtifact", + "{http://opentosca.org/nodetypes}PythonApp_3-w1", + ], ]); export function getNodeTypeQName(artifactTypeQName) { - return nodeTypeQNameMapping.get(artifactTypeQName); + return nodeTypeQNameMapping.get(artifactTypeQName); } export async function loadTopology(deploymentModelUrl) { - if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { - deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); + if (deploymentModelUrl.startsWith("{{ wineryEndpoint }}")) { + deploymentModelUrl = deploymentModelUrl.replace( + "{{ wineryEndpoint }}", + config.getWineryEndpoint() + ); + } + let topology; + let tags; + try { + topology = await fetch( + deploymentModelUrl.replace("?csar", "topologytemplate") + ).then((res) => res.json()); + tags = await fetch(deploymentModelUrl.replace("?csar", "tags")).then( + (res) => res.json() + ); + } catch (e) { + throw new Error( + "An unexpected error occurred during loading the deployments models topology." + ); + } + let topNode; + const topNodeTag = tags.find((tag) => tag.name === "top-node"); + if (topNodeTag) { + const topNodeId = topNodeTag.value; + topNode = topology.nodeTemplates.find( + (nodeTemplate) => nodeTemplate.id === topNodeId + ); + if (!topNode) { + throw new Error(`Top level node "${topNodeId}" not found.`); } - let topology; - let tags; - try { - topology = await fetch(deploymentModelUrl.replace('?csar', 'topologytemplate')) - .then(res => res.json()); - tags = await fetch(deploymentModelUrl.replace('?csar', 'tags')) - .then(res => res.json()); - - } catch (e) { - throw new Error('An unexpected error occurred during loading the deployments models topology.'); + } else { + let nodes = new Map( + topology.nodeTemplates.map((nodeTemplate) => [ + nodeTemplate.id, + nodeTemplate, + ]) + ); + for (let relationship of topology.relationshipTemplates) { + if (relationship.name === "HostedOn") { + nodes.delete(relationship.targetElement.ref); + } } - let topNode; - const topNodeTag = tags.find(tag => tag.name === "top-node"); - if (topNodeTag) { - const topNodeId = topNodeTag.value; - topNode = topology.nodeTemplates.find(nodeTemplate => nodeTemplate.id === topNodeId); - if (!topNode) { - throw new Error(`Top level node "${topNodeId}" not found.`); - } - } else { - let nodes = new Map(topology.nodeTemplates.map(nodeTemplate => [nodeTemplate.id, nodeTemplate])); - for (let relationship of topology.relationshipTemplates) { - if (relationship.name === "HostedOn") { - nodes.delete(relationship.targetElement.ref); - } - } - if (nodes.size === 1) { - topNode = nodes.values().next().value; - } - } - if (!topNode) { - throw new Error("No top level node found."); + if (nodes.size === 1) { + topNode = nodes.values().next().value; } + } + if (!topNode) { + throw new Error("No top level node found."); + } - return { - topNode, - nodeTemplates: topology.nodeTemplates, - relationshipTemplates: topology.relationshipTemplates - }; -} \ No newline at end of file + return { + topNode, + nodeTemplates: topology.nodeTemplates, + relationshipTemplates: topology.relationshipTemplates, + }; +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js index e9614967..0e11761f 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config-manager.js @@ -10,7 +10,7 @@ */ import defaultConfig from "./config"; -import {getPluginConfig} from '../../../editor/plugin/PluginConfigHandler'; +import { getPluginConfig } from "../../../editor/plugin/PluginConfigHandler"; let config = {}; @@ -20,12 +20,13 @@ let config = {}; * @return {string} the currently specified endpoint of the OpenTOSCA container */ export function getOpenTOSCAEndpoint() { - if (config.opentoscaEndpoint === undefined) { - setOpenTOSCAEndpoint( - getPluginConfig('opentosca').opentoscaEndpoint - || defaultConfig.opentoscaEndpoint); - } - return config.opentoscaEndpoint; + if (config.opentoscaEndpoint === undefined) { + setOpenTOSCAEndpoint( + getPluginConfig("opentosca").opentoscaEndpoint || + defaultConfig.opentoscaEndpoint + ); + } + return config.opentoscaEndpoint; } /** @@ -34,9 +35,9 @@ export function getOpenTOSCAEndpoint() { * @param opentoscaEndpoint the endpoint of the OpenTOSCA container */ export function setOpenTOSCAEndpoint(opentoscaEndpoint) { - if (opentoscaEndpoint !== null && opentoscaEndpoint !== undefined) { - config.opentoscaEndpoint = opentoscaEndpoint.replace(/\/$/, ''); - } + if (opentoscaEndpoint !== null && opentoscaEndpoint !== undefined) { + config.opentoscaEndpoint = opentoscaEndpoint.replace(/\/$/, ""); + } } /** @@ -45,12 +46,13 @@ export function setOpenTOSCAEndpoint(opentoscaEndpoint) { * @return {string} the currently specified endpoint of the Winery */ export function getWineryEndpoint() { - if (config.wineryEndpoint === undefined) { - setWineryEndpoint( - getPluginConfig('opentosca').wineryEndpoint - || defaultConfig.wineryEndpoint); - } - return config.wineryEndpoint; + if (config.wineryEndpoint === undefined) { + setWineryEndpoint( + getPluginConfig("opentosca").wineryEndpoint || + defaultConfig.wineryEndpoint + ); + } + return config.wineryEndpoint; } /** @@ -59,9 +61,9 @@ export function getWineryEndpoint() { * @param wineryEndpoint the endpoint of the Winery */ export function setWineryEndpoint(wineryEndpoint) { - if (wineryEndpoint !== null && wineryEndpoint !== undefined) { - config.wineryEndpoint = wineryEndpoint.replace(/\/$/, ''); - } + if (wineryEndpoint !== null && wineryEndpoint !== undefined) { + config.wineryEndpoint = wineryEndpoint.replace(/\/$/, ""); + } } /** @@ -69,5 +71,5 @@ export function setWineryEndpoint(wineryEndpoint) { * by setting this.comfig to an empty js object. */ export function resetConfig() { - config = {}; -} \ No newline at end of file + config = {}; +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js index 144ee909..9583a281 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/config.js @@ -11,7 +11,7 @@ // takes either the environment variables or the default values definded in webpack.config const defaultConfig = { - opentoscaEndpoint: process.env.OPENTOSCA_ENDPOINT, - wineryEndpoint: process.env.WINERY_ENDPOINT, + opentoscaEndpoint: process.env.OPENTOSCA_ENDPOINT, + wineryEndpoint: process.env.WINERY_ENDPOINT, }; -export default defaultConfig; \ No newline at end of file +export default defaultConfig; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js index 3acdba7f..fc12b5a2 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/framework-config/index.js @@ -8,7 +8,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -import * as configManager from './config-manager'; +import * as configManager from "./config-manager"; const config = configManager; export default config; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js index 40299fda..28861efa 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/OpenTOSCARenderer.js @@ -1,380 +1,462 @@ -import { - connectRectangles -} from 'diagram-js/lib/layout/ManhattanLayout'; - -import { - createLine, -} from 'diagram-js/lib/util/RenderUtil'; +import { connectRectangles } from "diagram-js/lib/layout/ManhattanLayout"; -import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer'; -import { getOrientation } from 'diagram-js/lib/layout/LayoutUtil'; +import { createLine } from "diagram-js/lib/util/RenderUtil"; +import BpmnRenderer from "bpmn-js/lib/draw/BpmnRenderer"; +import { getOrientation } from "diagram-js/lib/layout/LayoutUtil"; -import buttonIcon from '../resources/show-deployment-button.svg?raw'; -import {drawTaskSVG} from '../../../editor/util/RenderUtilities'; -import NotificationHandler from '../../../editor/ui/notifications/NotificationHandler'; -import {append as svgAppend, attr as svgAttr, create as svgCreate, select, prepend as svgPrepend} from 'tiny-svg'; -import {query as domQuery} from 'min-dom'; +import buttonIcon from "../resources/show-deployment-button.svg?raw"; +import { drawTaskSVG } from "../../../editor/util/RenderUtilities"; +import NotificationHandler from "../../../editor/ui/notifications/NotificationHandler"; import { - isSnapped, - setSnapped, - topLeft, - bottomRight -} from 'diagram-js/lib/features/snapping/SnapUtil'; + append as svgAppend, + attr as svgAttr, + create as svgCreate, + select, + prepend as svgPrepend, +} from "tiny-svg"; +import { query as domQuery } from "min-dom"; -import {loadTopology} from "../deployment/WineryUtils"; +import { loadTopology } from "../deployment/WineryUtils"; const HIGH_PRIORITY = 14001; -const SERVICE_TASK_TYPE = 'bpmn:ServiceTask'; -const DEPLOYMENT_GROUP_ID = 'deployment'; -const DEPLOYMENT_REL_MARKER_ID = 'deployment-rel'; +const SERVICE_TASK_TYPE = "bpmn:ServiceTask"; +const DEPLOYMENT_GROUP_ID = "deployment"; +const DEPLOYMENT_REL_MARKER_ID = "deployment-rel"; const NODE_WIDTH = 100; const NODE_HEIGHT = 60; const NODE_SHIFT_MARGIN = 10; const STROKE_STYLE = { - strokeLinecap: 'round', - strokeLinejoin: 'round', - stroke: '#777777', - strokeWidth: 2, - strokeDasharray: 4, + strokeLinecap: "round", + strokeLinejoin: "round", + stroke: "#777777", + strokeWidth: 2, + strokeDasharray: 4, }; export default class OpenTOSCARenderer extends BpmnRenderer { - constructor(config, eventBus, styles, pathMap, canvas, textRenderer, commandStack, elementRegistry) { - super(config, eventBus, styles, pathMap, canvas, textRenderer, HIGH_PRIORITY); - this.commandStack = commandStack; - this.elementRegistry = elementRegistry; - this.styles = styles; - this.textRenderer = textRenderer; - - // snap boundary events - eventBus.on([ - 'create.move', - 'create.end', - 'shape.move.move', - 'shape.move.end' - ], 140000, function(event) { - var context = event.context, - canExecute = context.canExecute, - target = context.target; - - var canAttach = canExecute && (canExecute === 'attach' || canExecute.attach); - const isRelevantEvent = context.shape.type === "bpmn:IntermediateThrowEvent" || context.shape.type === "bpmn:BoundaryEvent"; - - if (canAttach && isRelevantEvent && context.target.businessObject.get('opentosca:deploymentModelUrl') && getOrientation(event, target, -30) === "bottom-right") { - // Prevent snapping on deployment visualisation toggle button - event.stopPropagation(); - } - }); - - this.openToscaHandlers = { - [SERVICE_TASK_TYPE]: function (self, parentGfx, element) { - const task = self.renderer('bpmn:ServiceTask')(parentGfx, element); - self.maybeAddShowDeploymentModelButton(parentGfx, element); - return task; - } - }; - this.addMarkerDefinition(canvas); - this.registerShowDeploymentModelHandler(); - this.currentlyShownDeploymentsModels = new Map(); - } - - registerShowDeploymentModelHandler() { - this.commandStack.register("deploymentModel.showAll", { - execute: ({showDeploymentModel}) => { - const elementsWithDeploymentModel = this.elementRegistry - .filter(element => element.businessObject.get('opentosca:deploymentModelUrl')); - const changed = []; - for (const element of elementsWithDeploymentModel) { - const wasShownDeploymentModel = !!element.showDeploymentModel; - element.showDeploymentModel = showDeploymentModel; - element.wasShownDeploymentModel = wasShownDeploymentModel; - if (wasShownDeploymentModel !== showDeploymentModel) { - changed.push(element); - } - } - return changed; - }, - - revert({showDeploymentModel}) { - return this.execute({showDeploymentModel: !showDeploymentModel}); - } - }); + constructor( + config, + eventBus, + styles, + pathMap, + canvas, + textRenderer, + commandStack, + elementRegistry + ) { + super( + config, + eventBus, + styles, + pathMap, + canvas, + textRenderer, + HIGH_PRIORITY + ); + this.commandStack = commandStack; + this.elementRegistry = elementRegistry; + this.styles = styles; + this.textRenderer = textRenderer; + + // snap boundary events + eventBus.on( + ["create.move", "create.end", "shape.move.move", "shape.move.end"], + 140000, + function (event) { + var context = event.context, + canExecute = context.canExecute, + target = context.target; + + var canAttach = + canExecute && (canExecute === "attach" || canExecute.attach); + const isRelevantEvent = + context.shape.type === "bpmn:IntermediateThrowEvent" || + context.shape.type === "bpmn:BoundaryEvent"; + + if ( + canAttach && + isRelevantEvent && + context.target.businessObject.get("opentosca:deploymentModelUrl") && + getOrientation(event, target, -30) === "bottom-right" + ) { + // Prevent snapping on deployment visualisation toggle button + event.stopPropagation(); + } + } + ); + + this.openToscaHandlers = { + [SERVICE_TASK_TYPE]: function (self, parentGfx, element) { + const task = self.renderer("bpmn:ServiceTask")(parentGfx, element); + self.maybeAddShowDeploymentModelButton(parentGfx, element); + return task; + }, + }; + this.addMarkerDefinition(canvas); + this.registerShowDeploymentModelHandler(); + this.currentlyShownDeploymentsModels = new Map(); + } + + registerShowDeploymentModelHandler() { + this.commandStack.register("deploymentModel.showAll", { + execute: ({ showDeploymentModel }) => { + const elementsWithDeploymentModel = this.elementRegistry.filter( + (element) => + element.businessObject.get("opentosca:deploymentModelUrl") + ); + const changed = []; + for (const element of elementsWithDeploymentModel) { + const wasShownDeploymentModel = !!element.showDeploymentModel; + element.showDeploymentModel = showDeploymentModel; + element.wasShownDeploymentModel = wasShownDeploymentModel; + if (wasShownDeploymentModel !== showDeploymentModel) { + changed.push(element); + } + } + return changed; + }, + + revert({ showDeploymentModel }) { + return this.execute({ showDeploymentModel: !showDeploymentModel }); + }, + }); + + this.commandStack.register("deploymentModel.show", { + execute: ({ element, showDeploymentModel }) => { + const wasShownDeploymentModel = !!element.showDeploymentModel; + element.showDeploymentModel = showDeploymentModel; + element.wasShownDeploymentModel = wasShownDeploymentModel; + if (wasShownDeploymentModel !== showDeploymentModel) { + return [element]; + } + return []; + }, - this.commandStack.register("deploymentModel.show", { - execute: ({element, showDeploymentModel}) => { - const wasShownDeploymentModel = !!element.showDeploymentModel; - element.showDeploymentModel = showDeploymentModel; - element.wasShownDeploymentModel = wasShownDeploymentModel; - if (wasShownDeploymentModel !== showDeploymentModel) { - return [element]; - } - return []; - }, - - revert({element, showDeploymentModel}) { - return this.execute({element, showDeploymentModel: !showDeploymentModel}); - } + revert({ element, showDeploymentModel }) { + return this.execute({ + element, + showDeploymentModel: !showDeploymentModel, }); + }, + }); + } + + addMarkerDefinition(canvas) { + const marker = svgCreate("marker", { + id: DEPLOYMENT_REL_MARKER_ID, + viewBox: "0 0 8 8", + refX: 8, + refY: 4, + markerWidth: 8, + markerHeight: 8, + orient: "auto", + }); + svgAppend( + marker, + svgCreate("path", { + d: "M 0 0 L 8 4 L 0 8", + ...this.styles.computeStyle({}, ["no-fill"], { + ...STROKE_STYLE, + strokeWidth: 1, + strokeDasharray: 2, + }), + }) + ); + + let defs = domQuery("defs", canvas._svg); + if (!defs) { + defs = svgCreate("defs"); + + svgPrepend(canvas._svg, defs); } - - addMarkerDefinition(canvas) { - const marker = svgCreate('marker', { - id: DEPLOYMENT_REL_MARKER_ID, - viewBox: '0 0 8 8', - refX: 8, - refY: 4, - markerWidth: 8, - markerHeight: 8, - orient: 'auto' - }); - svgAppend(marker, svgCreate('path', { - d: 'M 0 0 L 8 4 L 0 8', - ...this.styles.computeStyle({}, ['no-fill'], { - ...STROKE_STYLE, - strokeWidth: 1, - strokeDasharray: 2 - }) - })); - - let defs = domQuery('defs', canvas._svg); - if (!defs) { - defs = svgCreate('defs'); - - svgPrepend(canvas._svg, defs); - } - svgAppend(defs, marker); + svgAppend(defs, marker); + } + + maybeAddShowDeploymentModelButton(parentGfx, element) { + let deploymentModelUrl = element.businessObject.get( + "opentosca:deploymentModelUrl" + ); + if (!deploymentModelUrl) return; + + const button = drawTaskSVG( + parentGfx, + { + transform: "matrix(0.3, 0, 0, 0.3, 85, 65)", + svg: buttonIcon, + }, + null, + true + ); + button.style["pointer-events"] = "all"; + button.style["cursor"] = "pointer"; + button.addEventListener("click", (e) => { + e.preventDefault(); + element.deploymentModelTopology = undefined; + this.commandStack.execute("deploymentModel.show", { + element: element, + showDeploymentModel: !element.showDeploymentModel, + }); + }); + if (element.showDeploymentModel) { + this.showDeploymentModel(parentGfx, element, deploymentModelUrl); + } else { + this.removeDeploymentModel(parentGfx, element); } - - maybeAddShowDeploymentModelButton(parentGfx, element) { - let deploymentModelUrl = element.businessObject.get('opentosca:deploymentModelUrl'); - if (!deploymentModelUrl) return; - - const button = drawTaskSVG(parentGfx, { - transform: 'matrix(0.3, 0, 0, 0.3, 85, 65)', - svg: buttonIcon - }, null, true); - button.style['pointer-events'] = 'all'; - button.style['cursor'] = 'pointer'; - button.addEventListener('click', (e) => { - e.preventDefault(); - element.deploymentModelTopology = undefined; - this.commandStack.execute("deploymentModel.show", { - element: element, - showDeploymentModel: !element.showDeploymentModel - }); + } + + async showDeploymentModel(parentGfx, element, deploymentModelUrl) { + if ( + !element.deploymentModelTopology || + element.loadedDeploymentModelUrl !== deploymentModelUrl + ) { + try { + const topology = await loadTopology(deploymentModelUrl); + element.loadedDeploymentModelUrl = deploymentModelUrl; + element.deploymentModelTopology = topology; + } catch (e) { + element.showDeploymentModel = false; + element.loadedDeploymentModelUrl = null; + element.deploymentModelTopology = null; + this.removeDeploymentModel(parentGfx, element); + console.error(e); + NotificationHandler.getInstance().displayNotification({ + type: "warning", + title: "Could not load topology", + content: e.message, + duration: 2000, }); - if (element.showDeploymentModel) { - this.showDeploymentModel(parentGfx, element, deploymentModelUrl); - } else { - this.removeDeploymentModel(parentGfx, element); - } + return; + } } - - async showDeploymentModel(parentGfx, element, deploymentModelUrl) { - if (!element.deploymentModelTopology || element.loadedDeploymentModelUrl !== deploymentModelUrl) { - try { - const topology = await loadTopology(deploymentModelUrl); - element.loadedDeploymentModelUrl = deploymentModelUrl; - element.deploymentModelTopology = topology; - } catch (e) { - element.showDeploymentModel = false; - element.loadedDeploymentModelUrl = null; - element.deploymentModelTopology = null; - this.removeDeploymentModel(parentGfx, element); - console.error(e); - NotificationHandler.getInstance().displayNotification({ - type: 'warning', - title: 'Could not load topology', - content: e.message, - duration: 2000 - }); - return; - } - } - const groupDef = svgCreate('g', {id: DEPLOYMENT_GROUP_ID}); - parentGfx.append(groupDef); - - const {nodeTemplates, relationshipTemplates, topNode} = element.deploymentModelTopology; - - let ySubtract = parseInt(topNode.y); - let xSubtract = parseInt(topNode.x); - - const positions = new Map(); - for (let nodeTemplate of nodeTemplates) { - const position = { - x: (parseInt(nodeTemplate.x) - xSubtract) / 1.4, - y: (parseInt(nodeTemplate.y) - ySubtract) / 1.4, - }; - - positions.set(nodeTemplate.id, position); - if (nodeTemplate.id !== topNode.id) { - this.drawNodeTemplate(groupDef, nodeTemplate, position); - } - } - const boundingBox = { - left: Math.min(...[...positions.values()].map(p => p.x)) + element.x, - top: Math.min(...[...positions.values()].map(p => p.y)) + element.y, - right: Math.max(...[...positions.values()].map(p => p.x)) + NODE_WIDTH + element.x, - bottom: Math.max(...[...positions.values()].map(p => p.y)) + NODE_HEIGHT + element.y - }; - - const previousBoundingBox = this.currentlyShownDeploymentsModels.get(element.id)?.boundingBox; - if (JSON.stringify(previousBoundingBox) !== JSON.stringify(boundingBox)) { - this.mayBeMoveNeighborNodes(boundingBox, element); - } - - this.currentlyShownDeploymentsModels.set(element.id, { - boundingBox - }); - - for (let relationshipTemplate of relationshipTemplates) { - const start = positions.get(relationshipTemplate.sourceElement.ref); - const end = positions.get(relationshipTemplate.targetElement.ref); - this.drawRelationship(groupDef, - start, relationshipTemplate.sourceElement.ref === topNode.id, - end, relationshipTemplate.targetElement.ref === topNode.id); - } + const groupDef = svgCreate("g", { id: DEPLOYMENT_GROUP_ID }); + parentGfx.append(groupDef); + + const { nodeTemplates, relationshipTemplates, topNode } = + element.deploymentModelTopology; + + let ySubtract = parseInt(topNode.y); + let xSubtract = parseInt(topNode.x); + + const positions = new Map(); + for (let nodeTemplate of nodeTemplates) { + const position = { + x: (parseInt(nodeTemplate.x) - xSubtract) / 1.4, + y: (parseInt(nodeTemplate.y) - ySubtract) / 1.4, + }; + + positions.set(nodeTemplate.id, position); + if (nodeTemplate.id !== topNode.id) { + this.drawNodeTemplate(groupDef, nodeTemplate, position); + } } - - mayBeMoveNeighborNodes(newBoundingBox, element) { - let shifts = { - right: 0, - left: 0, - }; - for (const [otherElementId, otherDeploymentModel] of this.currentlyShownDeploymentsModels.entries()) { - if (otherElementId === element.id) continue; - const otherBoundingBox = otherDeploymentModel.boundingBox; - if (newBoundingBox.left < otherBoundingBox.right && newBoundingBox.right > otherBoundingBox.left && - newBoundingBox.top < otherBoundingBox.bottom && newBoundingBox.bottom > otherBoundingBox.top) { - const distRightShift = newBoundingBox.right - otherBoundingBox.left - shifts.right; - const distLeftShift = otherBoundingBox.right - newBoundingBox.left - shifts.left; - - if (distRightShift < distLeftShift && distRightShift > 0) { - shifts.right += distRightShift; - } else if (distLeftShift < distRightShift && distLeftShift > 0) { - shifts.left += distLeftShift; - } - } - } - - const allElements = this.elementRegistry.getAll(); - const commands = []; - - if (shifts.right || shifts.left) { - const xPosition = (newBoundingBox.left + newBoundingBox.right) / 2; - for (const otherElement of allElements) { - let otherXPosition = element.x + NODE_WIDTH / 2; - const otherElementBoundingBox = this.currentlyShownDeploymentsModels.get(otherElement.id)?.boundingBox; - if (otherElementBoundingBox) { - otherXPosition = (otherElementBoundingBox.left + otherElementBoundingBox.right) / 2; - } - let xShift; - if (shifts.right && otherXPosition >= xPosition && otherElement.id !== element.id) { - xShift = shifts.right + NODE_SHIFT_MARGIN; - } else if (shifts.left && otherXPosition <= xPosition && otherElement.id !== element.id) { - xShift = -shifts.left - NODE_SHIFT_MARGIN; - } else { - continue; - } - // Can not move elements without parent - if(!otherElement.parent) continue; - commands.push({ - cmd: 'shape.move', - context: { - shape: otherElement, - hints: {}, - delta: {x: xShift, y: 0} - } - }); - if (otherElementBoundingBox) { - otherElementBoundingBox.left += xShift; - otherElementBoundingBox.right += xShift; - } - } - } - - if (commands.length > 0) { - this.commandStack.execute('properties-panel.multi-command-executor', commands); - } + const boundingBox = { + left: Math.min(...[...positions.values()].map((p) => p.x)) + element.x, + top: Math.min(...[...positions.values()].map((p) => p.y)) + element.y, + right: + Math.max(...[...positions.values()].map((p) => p.x)) + + NODE_WIDTH + + element.x, + bottom: + Math.max(...[...positions.values()].map((p) => p.y)) + + NODE_HEIGHT + + element.y, + }; + + const previousBoundingBox = this.currentlyShownDeploymentsModels.get( + element.id + )?.boundingBox; + if (JSON.stringify(previousBoundingBox) !== JSON.stringify(boundingBox)) { + this.mayBeMoveNeighborNodes(boundingBox, element); } - removeDeploymentModel(parentGfx, element) { - this.currentlyShownDeploymentsModels.delete(element.id); - const group = select(parentGfx, '#' + DEPLOYMENT_GROUP_ID); - if (group) { - group.remove(); - } + this.currentlyShownDeploymentsModels.set(element.id, { + boundingBox, + }); + + for (let relationshipTemplate of relationshipTemplates) { + const start = positions.get(relationshipTemplate.sourceElement.ref); + const end = positions.get(relationshipTemplate.targetElement.ref); + this.drawRelationship( + groupDef, + start, + relationshipTemplate.sourceElement.ref === topNode.id, + end, + relationshipTemplate.targetElement.ref === topNode.id + ); } - - drawRelationship(parentGfx, start, startIsToplevel, end, endIsToplevel) { - const line = createLine(connectRectangles({ - width: NODE_WIDTH, - height: startIsToplevel ? 80 : NODE_HEIGHT, - ...start - }, { - width: NODE_WIDTH, - height: endIsToplevel ? 80 : NODE_HEIGHT, - ...end - }), this.styles.computeStyle({}, ['no-fill'], { - ...STROKE_STYLE, - markerEnd: `url(#${DEPLOYMENT_REL_MARKER_ID})` - }), 5); - parentGfx.prepend(line); + } + + mayBeMoveNeighborNodes(newBoundingBox, element) { + let shifts = { + right: 0, + left: 0, + }; + for (const [ + otherElementId, + otherDeploymentModel, + ] of this.currentlyShownDeploymentsModels.entries()) { + if (otherElementId === element.id) continue; + const otherBoundingBox = otherDeploymentModel.boundingBox; + if ( + newBoundingBox.left < otherBoundingBox.right && + newBoundingBox.right > otherBoundingBox.left && + newBoundingBox.top < otherBoundingBox.bottom && + newBoundingBox.bottom > otherBoundingBox.top + ) { + const distRightShift = + newBoundingBox.right - otherBoundingBox.left - shifts.right; + const distLeftShift = + otherBoundingBox.right - newBoundingBox.left - shifts.left; + + if (distRightShift < distLeftShift && distRightShift > 0) { + shifts.right += distRightShift; + } else if (distLeftShift < distRightShift && distLeftShift > 0) { + shifts.left += distLeftShift; + } + } } - drawNodeTemplate(parentGfx, nodeTemplate, position) { - const groupDef = svgCreate('g'); - svgAttr(groupDef, {transform: `matrix(1, 0, 0, 1, ${position.x.toFixed(2)}, ${position.y.toFixed(2)})`}); - const rect = svgCreate('rect', { - width: NODE_WIDTH, - height: NODE_HEIGHT, - fill: '#DDDDDD', - ...STROKE_STYLE - }); - - svgAppend(groupDef, rect); - - const text = this.textRenderer.createText(nodeTemplate.name, { - box: { - width: NODE_WIDTH, - height: NODE_HEIGHT, - }, - align: 'center-middle' + const allElements = this.elementRegistry.getAll(); + const commands = []; + + if (shifts.right || shifts.left) { + const xPosition = (newBoundingBox.left + newBoundingBox.right) / 2; + for (const otherElement of allElements) { + let otherXPosition = element.x + NODE_WIDTH / 2; + const otherElementBoundingBox = + this.currentlyShownDeploymentsModels.get( + otherElement.id + )?.boundingBox; + if (otherElementBoundingBox) { + otherXPosition = + (otherElementBoundingBox.left + otherElementBoundingBox.right) / 2; + } + let xShift; + if ( + shifts.right && + otherXPosition >= xPosition && + otherElement.id !== element.id + ) { + xShift = shifts.right + NODE_SHIFT_MARGIN; + } else if ( + shifts.left && + otherXPosition <= xPosition && + otherElement.id !== element.id + ) { + xShift = -shifts.left - NODE_SHIFT_MARGIN; + } else { + continue; + } + // Can not move elements without parent + if (!otherElement.parent) continue; + commands.push({ + cmd: "shape.move", + context: { + shape: otherElement, + hints: {}, + delta: { x: xShift, y: 0 }, + }, }); - svgAppend(groupDef, text); - parentGfx.append(groupDef); + if (otherElementBoundingBox) { + otherElementBoundingBox.left += xShift; + otherElementBoundingBox.right += xShift; + } + } } - renderer(type) { - return this.handlers[type]; + if (commands.length > 0) { + this.commandStack.execute( + "properties-panel.multi-command-executor", + commands + ); } + } - canRender(element) { - // only return true if handler for rendering is registered - return this.openToscaHandlers[element.type]; + removeDeploymentModel(parentGfx, element) { + this.currentlyShownDeploymentsModels.delete(element.id); + const group = select(parentGfx, "#" + DEPLOYMENT_GROUP_ID); + if (group) { + group.remove(); } - - drawShape(parentNode, element) { - if (element.type in this.openToscaHandlers) { - const h = this.openToscaHandlers[element.type]; - return h(this, parentNode, element); + } + + drawRelationship(parentGfx, start, startIsToplevel, end, endIsToplevel) { + const line = createLine( + connectRectangles( + { + width: NODE_WIDTH, + height: startIsToplevel ? 80 : NODE_HEIGHT, + ...start, + }, + { + width: NODE_WIDTH, + height: endIsToplevel ? 80 : NODE_HEIGHT, + ...end, } - return super.drawShape(parentNode, element); + ), + this.styles.computeStyle({}, ["no-fill"], { + ...STROKE_STYLE, + markerEnd: `url(#${DEPLOYMENT_REL_MARKER_ID})`, + }), + 5 + ); + parentGfx.prepend(line); + } + + drawNodeTemplate(parentGfx, nodeTemplate, position) { + const groupDef = svgCreate("g"); + svgAttr(groupDef, { + transform: `matrix(1, 0, 0, 1, ${position.x.toFixed( + 2 + )}, ${position.y.toFixed(2)})`, + }); + const rect = svgCreate("rect", { + width: NODE_WIDTH, + height: NODE_HEIGHT, + fill: "#DDDDDD", + ...STROKE_STYLE, + }); + + svgAppend(groupDef, rect); + + const text = this.textRenderer.createText(nodeTemplate.name, { + box: { + width: NODE_WIDTH, + height: NODE_HEIGHT, + }, + align: "center-middle", + }); + svgAppend(groupDef, text); + parentGfx.append(groupDef); + } + + renderer(type) { + return this.handlers[type]; + } + + canRender(element) { + // only return true if handler for rendering is registered + return this.openToscaHandlers[element.type]; + } + + drawShape(parentNode, element) { + if (element.type in this.openToscaHandlers) { + const h = this.openToscaHandlers[element.type]; + return h(this, parentNode, element); } + return super.drawShape(parentNode, element); + } } OpenTOSCARenderer.$inject = [ - 'config', - 'eventBus', - 'styles', - 'pathMap', - 'canvas', - 'textRenderer', - 'commandStack', - 'elementRegistry' + "config", + "eventBus", + "styles", + "pathMap", + "canvas", + "textRenderer", + "commandStack", + "elementRegistry", ]; - - - diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js index 814c71ca..bc61a866 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/index.js @@ -8,11 +8,11 @@ * * SPDX-License-Identifier: Apache-2.0 */ -import ServiceTaskPropertiesProvider from './properties-provider/ServiceTaskPropertiesProvider' -import OpenTOSCARenderer from './OpenTOSCARenderer'; +import ServiceTaskPropertiesProvider from "./properties-provider/ServiceTaskPropertiesProvider"; +import OpenTOSCARenderer from "./OpenTOSCARenderer"; export default { - __init__: ['openToscaRenderer', 'serviceTaskPropertyProvider'], - openToscaRenderer: ['type', OpenTOSCARenderer], - serviceTaskPropertyProvider: ['type', ServiceTaskPropertiesProvider], + __init__: ["openToscaRenderer", "serviceTaskPropertyProvider"], + openToscaRenderer: ["type", OpenTOSCARenderer], + serviceTaskPropertyProvider: ["type", ServiceTaskPropertiesProvider], }; diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js index df10dcb5..49b30800 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUpload.js @@ -1,29 +1,34 @@ -import {HeaderButton} from '@bpmn-io/properties-panel'; -import React from 'react'; -import ArtifactModal from './ArtifactUploadModal'; -import {createRoot} from 'react-dom/client'; -import './artifact-modal.css'; -import {useService} from "bpmn-js-properties-panel"; +import { HeaderButton } from "@bpmn-io/properties-panel"; +import React from "react"; +import ArtifactModal from "./ArtifactUploadModal"; +import { createRoot } from "react-dom/client"; +import "./artifact-modal.css"; +import { useService } from "bpmn-js-properties-panel"; /** * Entry to display the button which opens the Artifact Upload modal */ export function ArtifactUpload(props) { - const {translate, element} = props; - const commandStack = useService('commandStack'); + const { translate, element } = props; + const commandStack = useService("commandStack"); + const onClick = () => { + const root = createRoot(document.getElementById("modal-container")); + root.render( + root.unmount()} + element={element} + commandStack={commandStack} + /> + ); + }; - const onClick = () => { - const root = createRoot(document.getElementById("modal-container")); - root.render( root.unmount()} element={element} commandStack={commandStack}/>); - }; - - return HeaderButton({ - id: 'artifact-upload-button', - description: translate('Upload Artifact'), - className: "qwm-artifact-upload-btn", - children: translate('Upload Artifact'), - title: translate('Upload Artifact'), - onClick, - }); + return HeaderButton({ + id: "artifact-upload-button", + description: translate("Upload Artifact"), + className: "qwm-artifact-upload-btn", + children: translate("Upload Artifact"), + title: translate("Upload Artifact"), + onClick, + }); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js index 2a818167..62ed7067 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ArtifactUploadModal.js @@ -10,23 +10,22 @@ */ /* eslint-disable no-unused-vars */ -import React, {useState} from 'react'; -import Modal from '../../../../editor/ui/modal/Modal'; -import './artifact-modal.css'; -import '../../../../editor/config/config-modal.css'; +import React, { useState } from "react"; +import Modal from "../../../../editor/ui/modal/Modal"; +import "./artifact-modal.css"; +import "../../../../editor/config/config-modal.css"; import { - createArtifactTemplateWithFile, - createServiceTemplateWithNodeAndArtifact, - getNodeTypeQName, - getArtifactTemplateInfo, - insertTopNodeTag, - serviceTemplateExists, - addNodeWithArtifactToServiceTemplateByName, - deleteArtifactTemplate -} from '../../deployment/WineryUtils'; -import NotificationHandler from '../../../../editor/ui/notifications/NotificationHandler'; -import {getWineryEndpoint} from "../../framework-config/config-manager"; - + createArtifactTemplateWithFile, + createServiceTemplateWithNodeAndArtifact, + getNodeTypeQName, + getArtifactTemplateInfo, + insertTopNodeTag, + serviceTemplateExists, + addNodeWithArtifactToServiceTemplateByName, + deleteArtifactTemplate, +} from "../../deployment/WineryUtils"; +import NotificationHandler from "../../../../editor/ui/notifications/NotificationHandler"; +import { getWineryEndpoint } from "../../framework-config/config-manager"; // polyfill upcoming structural components const Title = Modal.Title; @@ -43,208 +42,270 @@ const Footer = Modal.Footer; * @returns {JSX.Element} The modal as React component * @constructor */ -export default function ArtifactUploadModal({onClose, element, commandStack}) { - const [uploadFile, setUploadFile] = useState(null); - const [textInputDockerImage, setTextInputDockerImage] = useState(''); - const [selectedTab, setSelectedTab] = useState("artifact"); - const [selectedOption, setSelectedOption] = useState(""); - const [selectedOptionName, setSelectedOptionName] = useState(""); - const [artifactTypes, setArtifactTypes] = useState([]); - const [acceptTypes, setAcceptTypes] = useState(''); - +export default function ArtifactUploadModal({ + onClose, + element, + commandStack, +}) { + const [uploadFile, setUploadFile] = useState(null); + const [textInputDockerImage, setTextInputDockerImage] = useState(""); + const [selectedTab, setSelectedTab] = useState("artifact"); + const [selectedOption, setSelectedOption] = useState(""); + const [selectedOptionName, setSelectedOptionName] = useState(""); + const [artifactTypes, setArtifactTypes] = useState([]); + const [acceptTypes, setAcceptTypes] = useState(""); - async function updateArtifactSelect() { - const response = await fetch(`${getWineryEndpoint()}/artifacttypes/?includeVersions=true`, { - headers: { - 'Accept': 'application/json', - }, - }).then(res => res.json()); + async function updateArtifactSelect() { + const response = await fetch( + `${getWineryEndpoint()}/artifacttypes/?includeVersions=true`, + { + headers: { + Accept: "application/json", + }, + } + ).then((res) => res.json()); - const artifactTypes = response - .filter(option => option.name.includes("WAR") || option.name.includes("PythonArchive")); - setArtifactTypes(artifactTypes); - } + const artifactTypes = response.filter( + (option) => + option.name.includes("WAR") || option.name.includes("PythonArchive") + ); + setArtifactTypes(artifactTypes); + } - const allowedFileTypes = { - zip: '.tar.gz', - war: '.war', - }; + const allowedFileTypes = { + zip: ".tar.gz", + war: ".war", + }; - async function createServiceTemplate() { - const {close: closeNotification} = NotificationHandler.getInstance().displayNotification({ - type: 'info', - title: 'Uploading Artifact...', - content: 'Uploading artifact for ' + element.id, - duration: 1000 * 60 * 60 // very long time out because this notification gets closed after the API calls are finished - }); - let serviceTemplateAddress; - let doesExist = false; - try { - const namePrefix = element.businessObject.name ?? ""; - const artifactTemplateName = `${namePrefix}ArtifactTemplate-${element.id}`; - await deleteArtifactTemplate(artifactTemplateName); - const artifactTemplateAddress = await createArtifactTemplateWithFile(artifactTemplateName, selectedOption, uploadFile); - const artifactTemplateInfo = await getArtifactTemplateInfo(artifactTemplateAddress); - const artifactTemplateQName = artifactTemplateInfo.serviceTemplateOrNodeTypeOrNodeTypeImplementation[0].type; - const nodeTypeQName = getNodeTypeQName(selectedOption); - const serviceTemplateName = `${namePrefix}ServiceTemplate-${element.id}`; - const doesExist = await serviceTemplateExists(serviceTemplateName); - if(doesExist) { - serviceTemplateAddress = await addNodeWithArtifactToServiceTemplateByName(serviceTemplateName, nodeTypeQName, - `${namePrefix}Node-${element.id}`, artifactTemplateQName, - `${namePrefix}Artifact-${element.id}`, selectedOption); - } else { - serviceTemplateAddress = await createServiceTemplateWithNodeAndArtifact(serviceTemplateName, nodeTypeQName, - `${namePrefix}Node-${element.id}`, artifactTemplateQName, - `${namePrefix}Artifact-${element.id}`, selectedOption); - } - await insertTopNodeTag(serviceTemplateAddress, nodeTypeQName); - } catch (e) { - setTimeout(closeNotification, 1); - NotificationHandler.getInstance().displayNotification({ - type: 'error', - title: 'Service Template Creation failed', - content: 'Service Template could not be created due to an internal error.', - duration: 2000 - }); - return; - } - const deploymentModelUrl = `{{ wineryEndpoint }}/servicetemplates/${serviceTemplateAddress}?csar`; - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: element.businessObject, - properties: { - 'opentosca:deploymentModelUrl': deploymentModelUrl - } - }); - setTimeout(closeNotification, 1); - const content = doesExist ? 'updated' : 'created'; - NotificationHandler.getInstance().displayNotification({ - type: 'info', - title: 'Service Template ' + content.charAt(0).toUpperCase() + content.slice(1), - content: 'Service Template including Nodetype with attached Deployment Artifact of chosen type was successfully ' + content + '.', - duration: 4000 - }); + async function createServiceTemplate() { + const { close: closeNotification } = + NotificationHandler.getInstance().displayNotification({ + type: "info", + title: "Uploading Artifact...", + content: "Uploading artifact for " + element.id, + duration: 1000 * 60 * 60, // very long time out because this notification gets closed after the API calls are finished + }); + let serviceTemplateAddress; + let doesExist = false; + try { + const namePrefix = element.businessObject.name ?? ""; + const artifactTemplateName = `${namePrefix}ArtifactTemplate-${element.id}`; + await deleteArtifactTemplate(artifactTemplateName); + const artifactTemplateAddress = await createArtifactTemplateWithFile( + artifactTemplateName, + selectedOption, + uploadFile + ); + const artifactTemplateInfo = await getArtifactTemplateInfo( + artifactTemplateAddress + ); + const artifactTemplateQName = + artifactTemplateInfo + .serviceTemplateOrNodeTypeOrNodeTypeImplementation[0].type; + const nodeTypeQName = getNodeTypeQName(selectedOption); + const serviceTemplateName = `${namePrefix}ServiceTemplate-${element.id}`; + const doesExist = await serviceTemplateExists(serviceTemplateName); + if (doesExist) { + serviceTemplateAddress = + await addNodeWithArtifactToServiceTemplateByName( + serviceTemplateName, + nodeTypeQName, + `${namePrefix}Node-${element.id}`, + artifactTemplateQName, + `${namePrefix}Artifact-${element.id}`, + selectedOption + ); + } else { + serviceTemplateAddress = await createServiceTemplateWithNodeAndArtifact( + serviceTemplateName, + nodeTypeQName, + `${namePrefix}Node-${element.id}`, + artifactTemplateQName, + `${namePrefix}Artifact-${element.id}`, + selectedOption + ); + } + await insertTopNodeTag(serviceTemplateAddress, nodeTypeQName); + } catch (e) { + setTimeout(closeNotification, 1); + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Service Template Creation failed", + content: + "Service Template could not be created due to an internal error.", + duration: 2000, + }); + return; } + const deploymentModelUrl = `{{ wineryEndpoint }}/servicetemplates/${serviceTemplateAddress}?csar`; + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: element.businessObject, + properties: { + "opentosca:deploymentModelUrl": deploymentModelUrl, + }, + }); + setTimeout(closeNotification, 1); + const content = doesExist ? "updated" : "created"; + NotificationHandler.getInstance().displayNotification({ + type: "info", + title: + "Service Template " + + content.charAt(0).toUpperCase() + + content.slice(1), + content: + "Service Template including Nodetype with attached Deployment Artifact of chosen type was successfully " + + content + + ".", + duration: 4000, + }); + } + const onSubmit = async () => { + // Process the uploaded file or text input here + console.log("Uploaded file:", uploadFile); + console.log("Text input:", textInputDockerImage); + if (selectedTab === "artifact") { + if (uploadFile !== null && selectedOption !== "") { + onClose(); + await createServiceTemplate(); + } else { + onClose(); + setTimeout(function () { + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "No file selected!", + content: "Please select a file to create an artifact!", + duration: 4000, + }); + }, 300); + } + } + }; - const onSubmit = async () => { - // Process the uploaded file or text input here - console.log('Uploaded file:', uploadFile); - console.log('Text input:', textInputDockerImage); - if (selectedTab === "artifact") { - if (uploadFile !== null && selectedOption !== "") { - onClose(); - await createServiceTemplate(); - } else { - onClose(); - setTimeout(function () { - NotificationHandler.getInstance().displayNotification({ - type: 'error', - title: 'No file selected!', - content: 'Please select a file to create an artifact!', - duration: 4000 - }); - }, 300); - - } - } - }; - - const handleOptionChange = (e) => { - const {value} = e.target; - setSelectedOption(value); - setSelectedOptionName(artifactTypes.find(x => x.qName === value).name); - if (value.includes("WAR")) { - setAcceptTypes(allowedFileTypes.war); - } else if (value.includes("PythonArchive")) { - setAcceptTypes(allowedFileTypes.zip); - } - }; - - const isOptionSelected = selectedOption !== ""; - - if (artifactTypes.length === 0) { - updateArtifactSelect(); + const handleOptionChange = (e) => { + const { value } = e.target; + setSelectedOption(value); + setSelectedOptionName(artifactTypes.find((x) => x.qName === value).name); + if (value.includes("WAR")) { + setAcceptTypes(allowedFileTypes.war); + } else if (value.includes("PythonArchive")) { + setAcceptTypes(allowedFileTypes.zip); } + }; + const isOptionSelected = selectedOption !== ""; - return ( - - Artifact Upload + if (artifactTypes.length === 0) { + updateArtifactSelect(); + } - -
-
-
setSelectedTab("artifact")} - > - Local File -
-
setSelectedTab("docker")} - style={{display: "none"}} - > - Docker Image -
-
+ return ( + + Artifact Upload - {selectedTab === "artifact" && ( -
-
-
- - -
- {isOptionSelected && ( -
-
- - setUploadFile(e.target.files[0])} - /> -
-
- )} -
-
- )} - {selectedTab === "docker" && ( -
- - setTextInputDockerImage(e.target.value)} - /> -
- )} -
- + +
+
+
setSelectedTab("artifact")} + > + Local File +
+
setSelectedTab("docker")} + style={{ display: "none" }} + > + Docker Image +
+
-
-
- - + {selectedTab === "artifact" && ( +
+
+
+ +
-
- - ); + {isOptionSelected && ( +
+
+ + setUploadFile(e.target.files[0])} + /> +
+
+ )} +
+
+ )} + {selectedTab === "docker" && ( +
+ + setTextInputDockerImage(e.target.value)} + /> +
+ )} +
+ + +
+
+ + +
+
+
+ ); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js index 33fb2197..6a851c30 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Connector.js @@ -13,223 +13,259 @@ import { getModeler } from "../../../../editor/ModelerHandler"; * * SPDX-License-Identifier: Apache-2.0 */ -const yaml = require('js-yaml'); +const yaml = require("js-yaml"); /** - * Entry to display the endpoints of the uploaded openapi specification for BPMN service task. + * Entry to display the endpoints of the uploaded openapi specification for BPMN service task. */ export function Connector({ element, translate, urls }) { + const modeling = useService("modeling"); + const debounce = useService("debounceInput"); + + let arrValues = []; + for (let i = 0; i < urls.length; i++) { + arrValues.push({ + label: urls[i], + value: urls[i], + }); + } + + const selectOptions = function () { + return arrValues; + }; + + const get = function () { + return element.businessObject.get("quantme:connectorUrl"); + }; + + const setValue = function (value) { + const moddle = getModeler().get("moddle"); + const entry = moddle.create( + "camunda:Entry", + { key: "Accept", value: "application/json" }, + { key: "Content-Type", value: "application/json" } + ); + + const map = moddle.create("camunda:Map", { entries: [entry] }); + + entry.$parent = map; + + const headersInputParameter = moddle.create("camunda:InputParameter", { + definition: map, + name: "headers", + }); + const methodInputParameter = moddle.create("camunda:InputParameter", { + name: "method", + value: "POST", + }); + const urlInputParameter = moddle.create("camunda:InputParameter", { + name: "url", + value: "", + }); + + let endpointParameters = determineInputParameters( + element.businessObject.yaml, + value + ); + let scriptValue = constructScript(endpointParameters); + const script = moddle.create("camunda:Script", { + scriptFormat: "Groovy", + value: scriptValue, + resource: "Inline", + }); + + const payloadInputParameter = moddle.create("camunda:InputParameter", { + definition: script, + name: "payload", + }); + let inputParameters = []; + inputParameters.push(headersInputParameter); + inputParameters.push(methodInputParameter); + inputParameters.push(urlInputParameter); + inputParameters.push(payloadInputParameter); - const modeling = useService('modeling'); - const debounce = useService('debounceInput'); - - let arrValues = []; - for (let i = 0; i < urls.length; i++) { - arrValues.push({ - label: urls[i], - value: urls[i] - }); - } - - const selectOptions = function (element) { - return arrValues; - } + let outputParameters = []; - const get = function () { - return element.businessObject.get('quantme:connectorUrl'); - }; - - const setValue = function (value) { - const moddle = getModeler().get('moddle'); - const entry = moddle.create("camunda:Entry", { key: "Accept", value: "application/json" }, { key: "Content-Type", value: "application/json" }); - - const map = moddle.create("camunda:Map", { entries: [entry] }); - - entry.$parent = map; - - const headersInputParameter = moddle.create("camunda:InputParameter", { - definition: map, name: 'headers' - }); - const methodInputParameter = moddle.create("camunda:InputParameter", { - name: 'method', value: 'POST' - }); - const urlInputParameter = moddle.create("camunda:InputParameter", { - name: 'url', value: '' - }); - - let endpointParameters = determineInputParameters(element.businessObject.yaml, value); - let scriptValue = constructScript(endpointParameters); - const script = moddle.create("camunda:Script", { scriptFormat: 'Groovy', value: scriptValue, resource: 'Inline' }); - - const payloadInputParameter = moddle.create("camunda:InputParameter", { - definition: script, name: 'payload' - }); - let inputParameters = []; - inputParameters.push(headersInputParameter); - inputParameters.push(methodInputParameter); - inputParameters.push(urlInputParameter); - inputParameters.push(payloadInputParameter) - - let outputParameters = []; - - outputParameters = determineOutputParameters(element.businessObject.yaml); - let camundaOutputParameters = constructCamundaOutputParameters(outputParameters); - - let inputOutput = moddle.create('camunda:InputOutput', { inputParameters: inputParameters, outputParameters: camundaOutputParameters }) - element.businessObject.extensionElements = moddle.create('bpmn:ExtensionElements', { - values: [ - moddle.create('camunda:Connector', { - connectorId: 'http-connector', - inputOutput: inputOutput - }), - ], - }); - return modeling.updateProperties(element, { connectorUrl: value || '' }); - }; - - - return <> - {()} - ; + outputParameters = determineOutputParameters(element.businessObject.yaml); + let camundaOutputParameters = + constructCamundaOutputParameters(outputParameters); + + let inputOutput = moddle.create("camunda:InputOutput", { + inputParameters: inputParameters, + outputParameters: camundaOutputParameters, + }); + element.businessObject.extensionElements = moddle.create( + "bpmn:ExtensionElements", + { + values: [ + moddle.create("camunda:Connector", { + connectorId: "http-connector", + inputOutput: inputOutput, + }), + ], + } + ); + return modeling.updateProperties(element, { connectorUrl: value || "" }); + }; + + return ( + <> + { + + } + + ); } function determineInputParameters(yamlData, schemePath) { - // Parse the YAML data - const data = yaml.load(yamlData); - - // Initialize an object to store the input parameters - const inputParameters = {}; - let scheme = ""; - - // Extract the request bodies and their parameters - for (const [path, methods] of Object.entries(data.paths)) { - if(path === schemePath) { - for (const [method, details] of Object.entries(methods)) { - if (details.requestBody) { - const requestBody = details.requestBody; - const content = requestBody.content; - for (const [contentType, contentDetails] of Object.entries(content)) { - if (contentDetails.schema) { - scheme = contentDetails.schema; - const properties = scheme.properties || {}; - inputParameters[path] = properties; - } - } + // Parse the YAML data + const data = yaml.load(yamlData); + + // Initialize an object to store the input parameters + const inputParameters = {}; + let scheme = ""; + + // Extract the request bodies and their parameters + for (const [path, methods] of Object.entries(data.paths)) { + if (path === schemePath) { + for (const details of Object.values(methods)) { + if (details.requestBody) { + const requestBody = details.requestBody; + const content = requestBody.content; + for (const contentDetails of Object.values(content)) { + if (contentDetails.schema) { + scheme = contentDetails.schema; + inputParameters[path] = scheme.properties || {}; } + } } - }} - - if (scheme.$ref) { - const document = yaml.load(yamlData); - scheme = String(scheme.$ref).replace('#/', '').replaceAll('/', '.'); - - // Access the dynamically determined schema - const schemaPath = scheme; - scheme = getObjectByPath(document, schemaPath); + } } - // Function to access an object property by path - function getObjectByPath(obj, path) { - const parts = path.split('.'); - let currentObj = obj; - for (const part of parts) { - if (!currentObj || !currentObj.hasOwnProperty(part)) { - return undefined; - } - currentObj = currentObj[part]; - } - return currentObj; + } + + if (scheme.$ref) { + const document = yaml.load(yamlData); + scheme = String(scheme.$ref).replace("#/", "").replaceAll("/", "."); + + // Access the dynamically determined schema + const schemaPath = scheme; + scheme = getObjectByPath(document, schemaPath); + } + // Function to access an object property by path + function getObjectByPath(obj, path) { + const parts = path.split("."); + let currentObj = obj; + for (const part of parts) { + if (!currentObj || !currentObj.hasOwnProperty(part)) { + return undefined; + } + currentObj = currentObj[part]; } + return currentObj; + } - // Access the properties of the schema - const properties = Object.keys(scheme.properties); - return properties; + // Access the properties of the schema + return Object.keys(scheme.properties); } function determineOutputParameters(yamlData) { - // Parse the YAML data - const data = yaml.load(yamlData); - - // Initialize an object to store the input parameters - let outputParameters = []; - - // Extract the request bodies and their parameters - for (const [path, methods] of Object.entries(data.paths)) { - for (const [method, details] of Object.entries(methods)) { - if (details.responses) { - const response = details.responses; - // Access the properties of the schema - // Access the schema referenced by "200" - const statusCode = "200"; - let schema = response[statusCode].content["application/json"].schema; - if(schema.$ref) { - const schemaPath = schema.$ref.replace("#/", "").replaceAll("/", "."); - schema = getObjectByPath2(data, schemaPath); - } - // Function to access an object property by path - function getObjectByPath2(obj, path) { - const parts = path.split('.'); - let currentObj = obj; - for (const part of parts) { - if (!currentObj || !currentObj.hasOwnProperty(part)) { - return undefined; - } - currentObj = currentObj[part]; - } - return currentObj; - } - // Access the properties of the schema - outputParameters = Object.keys(schema.properties); + // Parse the YAML data + const data = yaml.load(yamlData); + + // Initialize an object to store the input parameters + let outputParameters = []; + + // Extract the request bodies and their parameters + for (const methods of Object.values(data.paths)) { + for (const details of Object.values(methods)) { + if (details.responses) { + const response = details.responses; + // Access the properties of the schema + // Access the schema referenced by "200" + const statusCode = "200"; + let schema = response[statusCode].content["application/json"].schema; + if (schema.$ref) { + const schemaPath = schema.$ref.replace("#/", "").replaceAll("/", "."); + schema = getObjectByPath2(data, schemaPath); + } + // Function to access an object property by path + // eslint-disable-next-line no-inner-declarations + function getObjectByPath2(obj, path) { + const parts = path.split("."); + let currentObj = obj; + for (const part of parts) { + if (!currentObj || !currentObj.hasOwnProperty(part)) { + return undefined; } + currentObj = currentObj[part]; + } + return currentObj; } + // Access the properties of the schema + outputParameters = Object.keys(schema.properties); + } } - return outputParameters; + } + return outputParameters; } function constructCamundaOutputParameters(parameters) { - let outputParameters = []; - for (let param of parameters) { - let moddle = getModeler().get('moddle'); - const script = moddle.create("camunda:Script", { - scriptFormat: 'Groovy', value: 'def resp = connector.getVariable("response");\n' + - 'resp = new groovy.json.JsonSlurper().parseText(resp);\n' + 'def ' + param + ' = resp.get(' + param + ');\n' + 'println(' + param + ');\n' + 'return ' + param + ';', resource: 'Inline' - }); - - const outputParameter = moddle.create("camunda:OutputParameter", { - definition: script, name: param - }); - outputParameters.push(outputParameter); - - } - return outputParameters; + let outputParameters = []; + for (let param of parameters) { + let moddle = getModeler().get("moddle"); + const script = moddle.create("camunda:Script", { + scriptFormat: "Groovy", + value: + 'def resp = connector.getVariable("response");\n' + + "resp = new groovy.json.JsonSlurper().parseText(resp);\n" + + "def " + + param + + " = resp.get(" + + param + + ");\n" + + "println(" + + param + + ");\n" + + "return " + + param + + ";", + resource: "Inline", + }); + + const outputParameter = moddle.create("camunda:OutputParameter", { + definition: script, + name: param, + }); + outputParameters.push(outputParameter); + } + return outputParameters; } - - function constructScript(parameters) { - let script = 'import groovy.json.JsonBuilder;\n'; - let jsonString = 'def request = [:];\n'; - for (let param of parameters) { - script += 'def ' + param + ' = execution.getVariable("' + param + '");\n' +'println(' + param + ');\n'; - jsonString += 'request.put("' + param + '",' + param + ');\n'; - } - //jsonString = removeLastComma(jsonString); - jsonString += 'requeststring = new JsonBuilder(request).toPrettyString();\nreturn requeststring;'; - script += jsonString; - //script += 'myJson = JSON.stringify(myJson)\nmyJson = myJson'; - return script; + let script = "import groovy.json.JsonBuilder;\n"; + let jsonString = "def request = [:];\n"; + for (let param of parameters) { + script += + "def " + + param + + ' = execution.getVariable("' + + param + + '");\n' + + "println(" + + param + + ");\n"; + jsonString += 'request.put("' + param + '",' + param + ");\n"; + } + //jsonString = removeLastComma(jsonString); + jsonString += + "requeststring = new JsonBuilder(request).toPrettyString();\nreturn requeststring;"; + script += jsonString; + //script += 'myJson = JSON.stringify(myJson)\nmyJson = myJson'; + return script; } - -function removeLastComma(str) { - var lastIndex = str.lastIndexOf(","); - if (lastIndex === -1) { - return str; // If comma is not found, return the original string - } else { - return str.slice(0, lastIndex) + str.slice(lastIndex + 1); - } -} \ No newline at end of file diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js index 45ff745d..c668c841 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/Deployment.js @@ -1,7 +1,7 @@ -import {SelectEntry} from "@bpmn-io/properties-panel"; +import { SelectEntry } from "@bpmn-io/properties-panel"; import React from "@bpmn-io/properties-panel/preact/compat"; -import {useService} from "bpmn-js-properties-panel"; -import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; +import { useService } from "bpmn-js-properties-panel"; +import { getImplementationType } from "../../../quantme/utilities/ImplementationTypeHelperExtension"; /** * Copyright (c) 2023 Institute of Architecture of Application Systems - @@ -14,74 +14,97 @@ import {getImplementationType} from "../../../quantme/utilities/ImplementationTy * SPDX-License-Identifier: Apache-2.0 */ -const jquery = require('jquery'); +const jquery = require("jquery"); -const QUANTME_NAMESPACE_PULL = 'http://quantil.org/quantme/pull'; -const QUANTME_NAMESPACE_PUSH = 'http://quantil.org/quantme/push'; +const QUANTME_NAMESPACE_PULL = "http://quantil.org/quantme/pull"; +const QUANTME_NAMESPACE_PUSH = "http://quantil.org/quantme/push"; /** * Entry to display the custom Implementation option deployment for BPMN service task. Through this option you can define * a CSAR as implementation of a service task. */ -export function Deployment({element, translate, wineryEndpoint}) { +export function Deployment({ element, translate, wineryEndpoint }) { + const modeling = useService("modeling"); + const debounce = useService("debounceInput"); - const modeling = useService('modeling'); - const debounce = useService('debounceInput'); - - const selectOptions = function (element) { - const arrValues = []; - jquery.ajax({ - url: wineryEndpoint + '/servicetemplates/?grouped', - method: 'GET', - success: function (result) { - for (let i = 0; i < result.length; i++) { - if (result[i].text === QUANTME_NAMESPACE_PULL || result[i].text === QUANTME_NAMESPACE_PUSH) { - result[i].children.forEach(element => arrValues.push({ - label: element.text, - value: concatenateCsarEndpoint('{{ wineryEndpoint }}', result[i].id, element.text) - })); - } - } - }, - async: false - }); - if (arrValues.length === 0) { - arrValues.push({label: 'No CSARs available', value: ''}); + const selectOptions = function () { + const arrValues = []; + jquery.ajax({ + url: wineryEndpoint + "/servicetemplates/?grouped", + method: "GET", + success: function (result) { + for (let i = 0; i < result.length; i++) { + if ( + result[i].text === QUANTME_NAMESPACE_PULL || + result[i].text === QUANTME_NAMESPACE_PUSH + ) { + result[i].children.forEach((element) => + arrValues.push({ + label: element.text, + value: concatenateCsarEndpoint( + "{{ wineryEndpoint }}", + result[i].id, + element.text + ), + }) + ); + } } - return arrValues; - }; + }, + async: false, + }); + if (arrValues.length === 0) { + arrValues.push({ label: "No CSARs available", value: "" }); + } + return arrValues; + }; - const get = function () { - return element.businessObject.get('opentosca:deploymentModelUrl'); - }; + const get = function () { + return element.businessObject.get("opentosca:deploymentModelUrl"); + }; - const setValue = function (value) { - return modeling.updateProperties(element, {deploymentModelUrl: value || ''}); - }; + const setValue = function (value) { + return modeling.updateProperties(element, { + deploymentModelUrl: value || "", + }); + }; - const validate = function (values) { - return values === undefined || values===''? translate('Must provide a CSAR') : ''; - }; + const validate = function (values) { + return values === undefined || values === "" + ? translate("Must provide a CSAR") + : ""; + }; - const hidden = function () { - const implType = getImplementationType(element); - console.log('getImplementationType returns ' + implType); - return !(implType === 'deploymentModel'); - }; + const hidden = function () { + const implType = getImplementationType(element); + console.log("getImplementationType returns " + implType); + return !(implType === "deploymentModel"); + }; - return <> - {!hidden() && ()} - ; + return ( + <> + {!hidden() && ( + + )} + + ); } function concatenateCsarEndpoint(wineryEndpoint, namespace, csarName) { - return wineryEndpoint + '/servicetemplates/' + encodeURIComponent(namespace) + '/' + csarName + '/?csar'; + return ( + wineryEndpoint + + "/servicetemplates/" + + encodeURIComponent(namespace) + + "/" + + csarName + + "/?csar" + ); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js index 35be18ab..a380f564 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/DmnImplementationProps.js @@ -1,361 +1,363 @@ -import { - getBusinessObject -} from 'bpmn-js/lib/util/ModelUtil'; +import { getBusinessObject } from "bpmn-js/lib/util/ModelUtil"; import { - TextFieldEntry, - isTextFieldEntryEdited, - SelectEntry, - isSelectEntryEdited -} from '@bpmn-io/properties-panel'; -import {useService} from "bpmn-js-properties-panel"; -import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; + TextFieldEntry, + isTextFieldEntryEdited, + SelectEntry, + isSelectEntryEdited, +} from "@bpmn-io/properties-panel"; +import { useService } from "bpmn-js-properties-panel"; +import { getImplementationType } from "../../../quantme/utilities/ImplementationTypeHelperExtension"; export function DmnImplementationProps(props) { - const { - element - } = props; - - const entries = []; + const { element } = props; - const implementationType = getImplementationType(element); - const bindingType = getDecisionRefBinding(element); + const entries = []; - if (implementationType !== 'dmn') { - return entries; - } + const implementationType = getImplementationType(element); + const bindingType = getDecisionRefBinding(element); - // (1) decisionRef + if (implementationType !== "dmn") { + return entries; + } + + // (1) decisionRef + entries.push({ + id: "decisionRef", + component: DecisionRef, + isEdited: isTextFieldEntryEdited, + }); + + // (2) binding + entries.push({ + id: "decisionRefBinding", + component: Binding, + isEdited: isSelectEntryEdited, + }); + + // (3) version + if (bindingType === "version") { entries.push({ - id: 'decisionRef', - component: DecisionRef, - isEdited: isTextFieldEntryEdited + id: "decisionRefVersion", + component: Version, + isEdited: isTextFieldEntryEdited, }); + } - - // (2) binding + // (4) versionTag + if (bindingType === "versionTag") { entries.push({ - id: 'decisionRefBinding', - component: Binding, - isEdited: isSelectEntryEdited + id: "decisionRefVersionTag", + component: VersionTag, + isEdited: isTextFieldEntryEdited, }); - - // (3) version - if (bindingType === 'version') { - entries.push({ - id: 'decisionRefVersion', - component: Version, - isEdited: isTextFieldEntryEdited - }); - } - - // (4) versionTag - if (bindingType === 'versionTag') { - entries.push({ - id: 'decisionRefVersionTag', - component: VersionTag, - isEdited: isTextFieldEntryEdited - }); - } - - // (5) tenantId + } + + // (5) tenantId + entries.push({ + id: "decisionRefTenantId", + component: TenantId, + isEdited: isTextFieldEntryEdited, + }); + + // (6) resultVariable + entries.push({ + id: "decisionRefResultVariable", + component: ResultVariable, + isEdited: isTextFieldEntryEdited, + }); + + // (7) mapDecisionResult + if (getResultVariable(element)) { entries.push({ - id: 'decisionRefTenantId', - component: TenantId, - isEdited: isTextFieldEntryEdited + id: "mapDecisionResult", + component: MapDecisionResult, + isEdited: isSelectEntryEdited, }); + } - // (6) resultVariable - entries.push({ - id: 'decisionRefResultVariable', - component: ResultVariable, - isEdited: isTextFieldEntryEdited - }); - - // (7) mapDecisionResult - if (getResultVariable(element)) { - entries.push({ - id: 'mapDecisionResult', - component: MapDecisionResult, - isEdited: isSelectEntryEdited - }); - } - - return entries; + return entries; } function DecisionRef(props) { - const {element} = props; + const { element } = props; - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const businessObject = getBusinessObject(element); + const businessObject = getBusinessObject(element); - const getValue = () => { - return businessObject.get('camunda:decisionRef'); - }; + const getValue = () => { + return businessObject.get("camunda:decisionRef"); + }; - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:decisionRef': value || '' - } - }); - }; - - return TextFieldEntry({ - element, - id: 'decisionRef', - label: translate('Decision reference'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:decisionRef": value || "", + }, }); + }; + + return TextFieldEntry({ + element, + id: "decisionRef", + label: translate("Decision reference"), + getValue, + setValue, + debounce, + }); } function Binding(props) { - const {element} = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - - const getValue = () => { - return getDecisionRefBinding(element); - }; + const { element } = props; - const setValue = (value) => { - const businessObject = getBusinessObject(element); - - // reset version properties on binding type change - const updatedProperties = { - 'camunda:decisionRefVersion': undefined, - 'camunda:decisionRefVersionTag': undefined, - 'camunda:decisionRefBinding': value - }; - - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: updatedProperties - }); - }; + const commandStack = useService("commandStack"); + const translate = useService("translate"); - // Note: default is "latest", - // cf. https://docs.camunda.org/manual/latest/reference/bpmn20/custom-extensions/extension-attributes/#decisionrefbinding - const getOptions = () => { + const getValue = () => { + return getDecisionRefBinding(element); + }; - const options = [ - {value: 'deployment', label: translate('deployment')}, - {value: 'latest', label: translate('latest')}, - {value: 'version', label: translate('version')}, - {value: 'versionTag', label: translate('versionTag')} - ]; + const setValue = (value) => { + const businessObject = getBusinessObject(element); - return options; + // reset version properties on binding type change + const updatedProperties = { + "camunda:decisionRefVersion": undefined, + "camunda:decisionRefVersionTag": undefined, + "camunda:decisionRefBinding": value, }; - return SelectEntry({ - element, - id: 'decisionRefBinding', - label: translate('Binding'), - getValue, - setValue, - getOptions + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: updatedProperties, }); + }; + + // Note: default is "latest", + // cf. https://docs.camunda.org/manual/latest/reference/bpmn20/custom-extensions/extension-attributes/#decisionrefbinding + const getOptions = () => { + const options = [ + { value: "deployment", label: translate("deployment") }, + { value: "latest", label: translate("latest") }, + { value: "version", label: translate("version") }, + { value: "versionTag", label: translate("versionTag") }, + ]; + + return options; + }; + + return SelectEntry({ + element, + id: "decisionRefBinding", + label: translate("Binding"), + getValue, + setValue, + getOptions, + }); } function Version(props) { - const {element} = props; + const { element } = props; - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const businessObject = getBusinessObject(element); - - const getValue = () => { - return businessObject.get('camunda:decisionRefVersion'); - }; + const businessObject = getBusinessObject(element); - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:decisionRefVersion': value - } - }); - }; + const getValue = () => { + return businessObject.get("camunda:decisionRefVersion"); + }; - return TextFieldEntry({ - element, - id: 'decisionRefVersion', - label: translate('Version'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:decisionRefVersion": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "decisionRefVersion", + label: translate("Version"), + getValue, + setValue, + debounce, + }); } function VersionTag(props) { - const {element} = props; + const { element } = props; - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const businessObject = getBusinessObject(element); + const businessObject = getBusinessObject(element); - const getValue = () => { - return businessObject.get('camunda:decisionRefVersionTag'); - }; + const getValue = () => { + return businessObject.get("camunda:decisionRefVersionTag"); + }; - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:decisionRefVersionTag': value - } - }); - }; - - return TextFieldEntry({ - element, - id: 'decisionRefVersionTag', - label: translate('Version tag'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:decisionRefVersionTag": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "decisionRefVersionTag", + label: translate("Version tag"), + getValue, + setValue, + debounce, + }); } function TenantId(props) { - const {element} = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const { element } = props; - const businessObject = getBusinessObject(element); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const getValue = () => { - return businessObject.get('camunda:decisionRefTenantId'); - }; + const businessObject = getBusinessObject(element); - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:decisionRefTenantId': value - } - }); - }; + const getValue = () => { + return businessObject.get("camunda:decisionRefTenantId"); + }; - return TextFieldEntry({ - element, - id: 'decisionRefTenantId', - label: translate('Tenant ID'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:decisionRefTenantId": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "decisionRefTenantId", + label: translate("Tenant ID"), + getValue, + setValue, + debounce, + }); } function ResultVariable(props) { - const {element} = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); - - const businessObject = getBusinessObject(element); - - const getValue = () => { - return getResultVariable(businessObject); - }; - - // Note: camunda:mapDecisionResult got cleaned up in modeling behavior - // cf. https://github.com/camunda/camunda-bpmn-js/blob/main/lib/camunda-platform/features/modeling/behavior/UpdateResultVariableBehavior.js - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:resultVariable': value - } - }); - }; - - return TextFieldEntry({ - element, - id: 'decisionRefResultVariable', - label: translate('Result variable'), - getValue, - setValue, - debounce + const { element } = props; + + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); + + const businessObject = getBusinessObject(element); + + const getValue = () => { + return getResultVariable(businessObject); + }; + + // Note: camunda:mapDecisionResult got cleaned up in modeling behavior + // cf. https://github.com/camunda/camunda-bpmn-js/blob/main/lib/camunda-platform/features/modeling/behavior/UpdateResultVariableBehavior.js + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:resultVariable": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "decisionRefResultVariable", + label: translate("Result variable"), + getValue, + setValue, + debounce, + }); } function MapDecisionResult(props) { - const {element} = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); + const { element } = props; - const businessObject = getBusinessObject(element); - - const getValue = () => { - return businessObject.get('camunda:mapDecisionResult') || 'resultList'; - }; + const commandStack = useService("commandStack"); + const translate = useService("translate"); - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:mapDecisionResult': value - } - }); - }; + const businessObject = getBusinessObject(element); - // Note: default is "resultList", - // cf. https://docs.camunda.org/manual/latest/reference/bpmn20/custom-extensions/extension-attributes/#mapdecisionresult - const getOptions = () => { - const options = [ - {value: 'collectEntries', label: translate('collectEntries (List)')}, - {value: 'resultList', label: translate('resultList (List>)')}, - {value: 'singleEntry', label: translate('singleEntry (TypedValue)')}, - {value: 'singleResult', label: translate('singleResult (Map)')} - ]; - - return options; - }; + const getValue = () => { + return businessObject.get("camunda:mapDecisionResult") || "resultList"; + }; - return SelectEntry({ - element, - id: 'mapDecisionResult', - label: translate('Map decision result'), - getValue, - setValue, - getOptions + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:mapDecisionResult": value, + }, }); + }; + + // Note: default is "resultList", + // cf. https://docs.camunda.org/manual/latest/reference/bpmn20/custom-extensions/extension-attributes/#mapdecisionresult + const getOptions = () => { + const options = [ + { + value: "collectEntries", + label: translate("collectEntries (List)"), + }, + { + value: "resultList", + label: translate("resultList (List>)"), + }, + { value: "singleEntry", label: translate("singleEntry (TypedValue)") }, + { + value: "singleResult", + label: translate("singleResult (Map)"), + }, + ]; + + return options; + }; + + return SelectEntry({ + element, + id: "mapDecisionResult", + label: translate("Map decision result"), + getValue, + setValue, + getOptions, + }); } - // helper //////////////////// function getDecisionRefBinding(element) { - const businessObject = getBusinessObject(element); - return businessObject.get('camunda:decisionRefBinding') || 'latest'; + const businessObject = getBusinessObject(element); + return businessObject.get("camunda:decisionRefBinding") || "latest"; } function getResultVariable(element) { - const businessObject = getBusinessObject(element); - return businessObject.get('camunda:resultVariable'); -} \ No newline at end of file + const businessObject = getBusinessObject(element); + return businessObject.get("camunda:resultVariable"); +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js index 3b8d033f..63b3e8c2 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationProps.js @@ -1,18 +1,20 @@ -import { TextFieldEntry, isTextFieldEntryEdited } from '@bpmn-io/properties-panel'; -import { DmnImplementationProps } from './DmnImplementationProps'; -import { ImplementationTypeProps } from './ImplementationTypeProps'; -import { useService } from "bpmn-js-properties-panel"; -import { getImplementationType } from "../../../quantme/utilities/ImplementationTypeHelperExtension" import { - getServiceTaskLikeBusinessObject, -} from "../../../../editor/util/camunda-utils/ImplementationTypeUtils"; + isTextFieldEntryEdited, + TextFieldEntry, +} from "@bpmn-io/properties-panel"; +import { DmnImplementationProps } from "./DmnImplementationProps"; +import { ImplementationTypeProps } from "./ImplementationTypeProps"; +import { useService } from "bpmn-js-properties-panel"; +import { getImplementationType } from "../../../quantme/utilities/ImplementationTypeHelperExtension"; +import { getServiceTaskLikeBusinessObject } from "../../../../editor/util/camunda-utils/ImplementationTypeUtils"; import { getExtensionElementsList } from "../../../../editor/util/camunda-utils/ExtensionElementsUtil"; import { Deployment } from "./Deployment"; -import { Connector } from './Connector'; -import { YamlUpload } from './YamlUpload'; -import { ArtifactUpload } from './ArtifactUpload'; -const yaml = require('js-yaml'); -const QUANTME_NAMESPACE_PULL = 'http://quantil.org/quantme/pull'; +import { Connector } from "./Connector"; +import { YamlUpload } from "./YamlUpload"; +import { ArtifactUpload } from "./ArtifactUpload"; + +const yaml = require("js-yaml"); +const QUANTME_NAMESPACE_PULL = "http://quantil.org/quantme/pull"; /** * Properties group for service tasks. Extends the original implementation by adding a new selection option to the @@ -23,337 +25,324 @@ const QUANTME_NAMESPACE_PULL = 'http://quantil.org/quantme/pull'; * @constructor */ export function ImplementationProps(props) { - const { - element, - wineryEndpoint, - translate, - } = props; + const { element, wineryEndpoint, translate } = props; - if (!getServiceTaskLikeBusinessObject(element)) { - return []; - } + if (!getServiceTaskLikeBusinessObject(element)) { + return []; + } - const implementationType = getImplementationType(element); - - // (1) display implementation type select - const entries = [ - ...ImplementationTypeProps({ element }) - ]; - - // (2) display implementation properties based on type - if (implementationType === 'class') { - entries.push({ - id: 'javaClass', - component: JavaClass, - isEdited: isTextFieldEntryEdited - }); - } else if (implementationType === 'expression') { - entries.push( - { - id: 'expression', - component: Expression, - isEdited: isTextFieldEntryEdited - }, - { - id: 'expressionResultVariable', - component: ResultVariable, - isEdited: isTextFieldEntryEdited - } - ); - } else if (implementationType === 'delegateExpression') { - entries.push( - { - id: 'delegateExpression', - component: DelegateExpression, - isEdited: isTextFieldEntryEdited - } - ); - } else if (implementationType === 'dmn') { - entries.push(...DmnImplementationProps({ element })); - } else if (implementationType === 'external') { - entries.push( - { - id: 'externalTopic', - component: Topic, - isEdited: isTextFieldEntryEdited - } - ); - } else if (implementationType === 'connector') { - entries.push( - { - id: 'connectorId', - component: ConnectorId, - isEdited: isTextFieldEntryEdited - } - ); - - // custom extension - } else if (implementationType === 'deploymentModel') { - entries.push({ - id: 'deployment', - element, - translate, - wineryEndpoint, - component: Deployment, - isEdited: isTextFieldEntryEdited - }); - entries.push({ - id: 'yamlUpload', - component: YamlUpload, - isEdited: isTextFieldEntryEdited - }) - if (!element.businessObject.deploymentModelUrl.includes(encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PULL))) && element.businessObject.yaml !== undefined) { - const urls = extractUrlsFromYaml(element.businessObject.yaml); - entries.push({ - id: 'connector', - element, - translate, - urls, - component: Connector, - isEdited: isTextFieldEntryEdited - }) - } - entries.push({ - id: 'artifactUpload', - element, - translate, - component: ArtifactUpload, - isEdited: isTextFieldEntryEdited - }); - } + const implementationType = getImplementationType(element); + // (1) display implementation type select + const entries = [...ImplementationTypeProps({ element })]; -return entries; + // (2) display implementation properties based on type + if (implementationType === "class") { + entries.push({ + id: "javaClass", + component: JavaClass, + isEdited: isTextFieldEntryEdited, + }); + } else if (implementationType === "expression") { + entries.push( + { + id: "expression", + component: Expression, + isEdited: isTextFieldEntryEdited, + }, + { + id: "expressionResultVariable", + component: ResultVariable, + isEdited: isTextFieldEntryEdited, + } + ); + } else if (implementationType === "delegateExpression") { + entries.push({ + id: "delegateExpression", + component: DelegateExpression, + isEdited: isTextFieldEntryEdited, + }); + } else if (implementationType === "dmn") { + entries.push(...DmnImplementationProps({ element })); + } else if (implementationType === "external") { + entries.push({ + id: "externalTopic", + component: Topic, + isEdited: isTextFieldEntryEdited, + }); + } else if (implementationType === "connector") { + entries.push({ + id: "connectorId", + component: ConnectorId, + isEdited: isTextFieldEntryEdited, + }); + + // custom extension + } else if (implementationType === "deploymentModel") { + entries.push({ + id: "deployment", + element, + translate, + wineryEndpoint, + component: Deployment, + isEdited: isTextFieldEntryEdited, + }); + entries.push({ + id: "yamlUpload", + component: YamlUpload, + isEdited: isTextFieldEntryEdited, + }); + if ( + !element.businessObject.deploymentModelUrl.includes( + encodeURIComponent(encodeURIComponent(QUANTME_NAMESPACE_PULL)) + ) && + element.businessObject.yaml !== undefined + ) { + const urls = extractUrlsFromYaml(element.businessObject.yaml); + entries.push({ + id: "connector", + element, + translate, + urls, + component: Connector, + isEdited: isTextFieldEntryEdited, + }); + } + entries.push({ + id: "artifactUpload", + element, + translate, + component: ArtifactUpload, + isEdited: isTextFieldEntryEdited, + }); + } + + return entries; } function extractUrlsFromYaml(content) { - const doc = yaml.load(content); - - // Extract URLs from paths - const paths = Object.keys(doc.paths); - const urls = paths.map((path) => { - const method = Object.keys(doc.paths[path])[0]; - const url = `${path}`; - return url; - }); + const doc = yaml.load(content); - return urls; + // Extract URLs from paths + const paths = Object.keys(doc.paths); + return paths.map((path) => { + return `${path}`; + }); } export function JavaClass(props) { - const { - element, - businessObject = getServiceTaskLikeBusinessObject(element), - id = 'javaClass' - } = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); - - const getValue = () => { - return businessObject.get('camunda:class'); - }; - - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:class': value || '' - } - }); - }; - - return TextFieldEntry({ - element, - id, - label: translate('Java class'), - getValue, - setValue, - debounce + const { + element, + businessObject = getServiceTaskLikeBusinessObject(element), + id = "javaClass", + } = props; + + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); + + const getValue = () => { + return businessObject.get("camunda:class"); + }; + + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:class": value || "", + }, }); + }; + + return TextFieldEntry({ + element, + id, + label: translate("Java class"), + getValue, + setValue, + debounce, + }); } export function Expression(props) { - const { - element, - businessObject = getServiceTaskLikeBusinessObject(element), - id = 'expression' - } = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); - - const getValue = () => { - return businessObject.get('camunda:expression'); - }; - - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:expression': value || '' - } - }); - }; - - return TextFieldEntry({ - element, - id, - label: translate('Expression'), - getValue, - setValue, - debounce + const { + element, + businessObject = getServiceTaskLikeBusinessObject(element), + id = "expression", + } = props; + + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); + + const getValue = () => { + return businessObject.get("camunda:expression"); + }; + + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:expression": value || "", + }, }); + }; + + return TextFieldEntry({ + element, + id, + label: translate("Expression"), + getValue, + setValue, + debounce, + }); } function ResultVariable(props) { - const { element } = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const { element } = props; - const businessObject = getServiceTaskLikeBusinessObject(element); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const getValue = () => { - return businessObject.get('camunda:resultVariable'); - }; + const businessObject = getServiceTaskLikeBusinessObject(element); - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:resultVariable': value - } - }); - }; + const getValue = () => { + return businessObject.get("camunda:resultVariable"); + }; - return TextFieldEntry({ - element, - id: 'expressionResultVariable', - label: translate('Result variable'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:resultVariable": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "expressionResultVariable", + label: translate("Result variable"), + getValue, + setValue, + debounce, + }); } export function DelegateExpression(props) { - const { - element, - businessObject = getServiceTaskLikeBusinessObject(element), - id = 'delegateExpression' - } = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); - - const getValue = () => { - return businessObject.get('camunda:delegateExpression'); - }; - - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:delegateExpression': value || '' - } - }); - }; - - return TextFieldEntry({ - element, - id, - label: translate('Delegate expression'), - getValue, - setValue, - debounce + const { + element, + businessObject = getServiceTaskLikeBusinessObject(element), + id = "delegateExpression", + } = props; + + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); + + const getValue = () => { + return businessObject.get("camunda:delegateExpression"); + }; + + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:delegateExpression": value || "", + }, }); + }; + + return TextFieldEntry({ + element, + id, + label: translate("Delegate expression"), + getValue, + setValue, + debounce, + }); } function Topic(props) { - const { element } = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const { element } = props; - const businessObject = getServiceTaskLikeBusinessObject(element); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const getValue = () => { - return businessObject.get('camunda:topic'); - }; + const businessObject = getServiceTaskLikeBusinessObject(element); - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: businessObject, - properties: { - 'camunda:topic': value - } - }); - }; + const getValue = () => { + return businessObject.get("camunda:topic"); + }; - return TextFieldEntry({ - element, - id: 'externalTopic', - label: translate('Topic'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: businessObject, + properties: { + "camunda:topic": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "externalTopic", + label: translate("Topic"), + getValue, + setValue, + debounce, + }); } function ConnectorId(props) { - const { element } = props; - - const commandStack = useService('commandStack'); - const translate = useService('translate'); - const debounce = useService('debounceInput'); + const { element } = props; - const connector = getConnector(element); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + const debounce = useService("debounceInput"); - const getValue = () => { - return connector.get('camunda:connectorId'); - }; + const connector = getConnector(element); - const setValue = (value) => { - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: connector, - properties: { - 'camunda:connectorId': value - } - }); - }; + const getValue = () => { + return connector.get("camunda:connectorId"); + }; - return TextFieldEntry({ - element, - id: 'connectorId', - label: translate('Connector ID'), - getValue, - setValue, - debounce + const setValue = (value) => { + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: connector, + properties: { + "camunda:connectorId": value, + }, }); + }; + + return TextFieldEntry({ + element, + id: "connectorId", + label: translate("Connector ID"), + getValue, + setValue, + debounce, + }); } - // helper ////////////////// function getConnectors(businessObject) { - return getExtensionElementsList(businessObject, 'camunda:Connector'); + return getExtensionElementsList(businessObject, "camunda:Connector"); } function getConnector(element) { - const businessObject = getServiceTaskLikeBusinessObject(element); - const connectors = getConnectors(businessObject); + const businessObject = getServiceTaskLikeBusinessObject(element); + const connectors = getConnectors(businessObject); - return connectors[0]; + return connectors[0]; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js index 80cafc4c..5a8f74b6 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ImplementationTypeProps.js @@ -1,273 +1,290 @@ -import { - sortBy, without -} from 'min-dash'; +import { sortBy, without } from "min-dash"; -import {SelectEntry, isSelectEntryEdited} from '@bpmn-io/properties-panel'; -import {useService} from "bpmn-js-properties-panel"; -import {createElement} from "../../../../editor/util/camunda-utils/ElementUtil"; +import { SelectEntry, isSelectEntryEdited } from "@bpmn-io/properties-panel"; +import { useService } from "bpmn-js-properties-panel"; +import { createElement } from "../../../../editor/util/camunda-utils/ElementUtil"; import { - getServiceTaskLikeBusinessObject, isDeploymentCapable, - isDmnCapable, - isExternalCapable, isServiceTaskLike + getServiceTaskLikeBusinessObject, + isDeploymentCapable, + isDmnCapable, + isExternalCapable, + isServiceTaskLike, } from "../../../../editor/util/camunda-utils/ImplementationTypeUtils"; -import {getExtensionElementsList} from "../../../../editor/util/camunda-utils/ExtensionElementsUtil"; -import {getImplementationType} from "../../../quantme/utilities/ImplementationTypeHelperExtension"; - +import { getExtensionElementsList } from "../../../../editor/util/camunda-utils/ExtensionElementsUtil"; +import { getImplementationType } from "../../../quantme/utilities/ImplementationTypeHelperExtension"; const DELEGATE_PROPS = { - 'camunda:class': undefined, - 'camunda:expression': undefined, - 'camunda:delegateExpression': undefined, - 'camunda:resultVariable': undefined + "camunda:class": undefined, + "camunda:expression": undefined, + "camunda:delegateExpression": undefined, + "camunda:resultVariable": undefined, }; const DMN_CAPABLE_PROPS = { - 'camunda:decisionRef': undefined, - 'camunda:decisionRefBinding': 'latest', - 'camunda:decisionRefVersion': undefined, - 'camunda:mapDecisionResult': 'resultList', - 'camunda:decisionRefTenantId': undefined + "camunda:decisionRef": undefined, + "camunda:decisionRefBinding": "latest", + "camunda:decisionRefVersion": undefined, + "camunda:mapDecisionResult": "resultList", + "camunda:decisionRefTenantId": undefined, }; const EXTERNAL_CAPABLE_PROPS = { - 'camunda:type': undefined, - 'camunda:topic': undefined + "camunda:type": undefined, + "camunda:topic": undefined, }; -const IMPLEMENTATION_TYPE_NONE_LABEL = '', - IMPLEMENTATION_TYPE_JAVA_LABEL = 'Java class', - IMPLEMENTATION_TYPE_EXPRESSION_LABEL = 'Expression', - IMPLEMENTATION_TYPE_DELEGATE_LABEL = 'Delegate expression', - IMPLEMENTATION_TYPE_DMN_LABEL = 'DMN', - IMPLEMENTATION_TYPE_EXTERNAL_LABEL = 'External', - IMPLEMENTATION_TYPE_CONNECTOR_LABEL = 'Connector', - IMPLEMENTATION_TYPE_DEPLOYMENT_LABEL = 'Deployment Model'; - - -export function ImplementationTypeProps(props) { - return [ - { - id: 'implementationType', - component: ImplementationType, - isEdited: isSelectEntryEdited - }, - ]; +const IMPLEMENTATION_TYPE_NONE_LABEL = "", + IMPLEMENTATION_TYPE_JAVA_LABEL = "Java class", + IMPLEMENTATION_TYPE_EXPRESSION_LABEL = "Expression", + IMPLEMENTATION_TYPE_DELEGATE_LABEL = "Delegate expression", + IMPLEMENTATION_TYPE_DMN_LABEL = "DMN", + IMPLEMENTATION_TYPE_EXTERNAL_LABEL = "External", + IMPLEMENTATION_TYPE_CONNECTOR_LABEL = "Connector", + IMPLEMENTATION_TYPE_DEPLOYMENT_LABEL = "Deployment Model"; + +export function ImplementationTypeProps() { + return [ + { + id: "implementationType", + component: ImplementationType, + isEdited: isSelectEntryEdited, + }, + ]; } - function ImplementationType(props) { - const {element} = props; - - const bpmnFactory = useService('bpmnFactory'); - const commandStack = useService('commandStack'); - const translate = useService('translate'); - - const getValue = () => { - return getImplementationType(element) || ''; - }; - - const setValue = (value) => { - - const oldType = getImplementationType(element); - const businessObject = getServiceTaskLikeBusinessObject(element); - const commands = []; - - let updatedProperties = DELEGATE_PROPS; - let extensionElements = businessObject.get('extensionElements'); - - // (1) class, expression, delegateExpression - if (isDelegateType(value)) { - - updatedProperties = { - ...updatedProperties, - [value]: isDelegateType(oldType) ? businessObject.get(`camunda:${oldType}`) : '' - }; - - } - - // (2) dmn - if (isDmnCapable(businessObject)) { - updatedProperties = { - ...updatedProperties, - ...DMN_CAPABLE_PROPS - }; - - if (value === 'dmn') { - updatedProperties = { - ...updatedProperties, - 'camunda:decisionRef': '' - }; - } - } - - // (3) external - // Note: error event definition elements got cleaned up in modeling behavior - // cf. https://github.com/camunda/camunda-bpmn-js/blob/main/lib/camunda-platform/features/modeling/behavior/DeleteErrorEventDefinitionBehavior.js - if (isExternalCapable(businessObject)) { - updatedProperties = { - ...updatedProperties, - ...EXTERNAL_CAPABLE_PROPS - }; - - if (value === 'external') { - updatedProperties = { - ...updatedProperties, - 'camunda:type': 'external', - 'camunda:topic': '' - }; - } - } - - // (4) connector - if (isServiceTaskLike(businessObject)) { - - // (4.1) remove all connectors on type change - const connectors = getConnectors(businessObject); - - if (connectors.length) { - commands.push({ - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: extensionElements, - properties: { - values: without(extensionElements.get('values'), value => connectors.includes(value)) - } - } - }); - } - - // (4.2) create connector - if (value === 'connector') { - - // ensure extension elements - if (!extensionElements) { - extensionElements = createElement( - 'bpmn:ExtensionElements', - {values: []}, - businessObject, - bpmnFactory - ); - - commands.push(UpdateModdlePropertiesCommand(element, businessObject, {extensionElements})); - } - - const connector = createElement( - 'camunda:Connector', - {}, - extensionElements, - bpmnFactory - ); - - commands.push({ - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: extensionElements, - properties: { - values: [...extensionElements.get('values'), connector] - } - } - }); - } - - } - - // (5) deployment - if (isDeploymentCapable(businessObject)) { - updatedProperties = { - ...updatedProperties, - 'opentosca:deploymentModelUrl': undefined - }; - - if (value === 'deploymentModel') { - updatedProperties = { - ...updatedProperties, - 'opentosca:deploymentModelUrl': '' - }; - } - } - - - // (5) collect all property updates - commands.push(UpdateModdlePropertiesCommand(element, businessObject, updatedProperties)); - - // (6) commit all updates - commandStack.execute('properties-panel.multi-command-executor', commands); - }; - - const getOptions = () => { - const businessObject = getServiceTaskLikeBusinessObject(element); - - const options = [ - {value: '', label: translate(IMPLEMENTATION_TYPE_NONE_LABEL)}, - {value: 'class', label: translate(IMPLEMENTATION_TYPE_JAVA_LABEL)}, - {value: 'expression', label: translate(IMPLEMENTATION_TYPE_EXPRESSION_LABEL)}, - {value: 'delegateExpression', label: translate(IMPLEMENTATION_TYPE_DELEGATE_LABEL)} - ]; - - if (isDmnCapable(businessObject)) { - options.push({value: 'dmn', label: translate(IMPLEMENTATION_TYPE_DMN_LABEL)}); - } - - if (isExternalCapable(businessObject)) { - options.push({value: 'external', label: translate(IMPLEMENTATION_TYPE_EXTERNAL_LABEL)}); + const { element } = props; + + const bpmnFactory = useService("bpmnFactory"); + const commandStack = useService("commandStack"); + const translate = useService("translate"); + + const getValue = () => { + return getImplementationType(element) || ""; + }; + + const setValue = (value) => { + const oldType = getImplementationType(element); + const businessObject = getServiceTaskLikeBusinessObject(element); + const commands = []; + + let updatedProperties = DELEGATE_PROPS; + let extensionElements = businessObject.get("extensionElements"); + + // (1) class, expression, delegateExpression + if (isDelegateType(value)) { + updatedProperties = { + ...updatedProperties, + [value]: isDelegateType(oldType) + ? businessObject.get(`camunda:${oldType}`) + : "", + }; + } + + // (2) dmn + if (isDmnCapable(businessObject)) { + updatedProperties = { + ...updatedProperties, + ...DMN_CAPABLE_PROPS, + }; + + if (value === "dmn") { + updatedProperties = { + ...updatedProperties, + "camunda:decisionRef": "", + }; + } + } + + // (3) external + // Note: error event definition elements got cleaned up in modeling behavior + // cf. https://github.com/camunda/camunda-bpmn-js/blob/main/lib/camunda-platform/features/modeling/behavior/DeleteErrorEventDefinitionBehavior.js + if (isExternalCapable(businessObject)) { + updatedProperties = { + ...updatedProperties, + ...EXTERNAL_CAPABLE_PROPS, + }; + + if (value === "external") { + updatedProperties = { + ...updatedProperties, + "camunda:type": "external", + "camunda:topic": "", + }; + } + } + + // (4) connector + if (isServiceTaskLike(businessObject)) { + // (4.1) remove all connectors on type change + const connectors = getConnectors(businessObject); + + if (connectors.length) { + commands.push({ + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: extensionElements, + properties: { + values: without(extensionElements.get("values"), (value) => + connectors.includes(value) + ), + }, + }, + }); + } + + // (4.2) create connector + if (value === "connector") { + // ensure extension elements + if (!extensionElements) { + extensionElements = createElement( + "bpmn:ExtensionElements", + { values: [] }, + businessObject, + bpmnFactory + ); + + commands.push( + UpdateModdlePropertiesCommand(element, businessObject, { + extensionElements, + }) + ); } - if (isServiceTaskLike(businessObject)) { - options.push({value: 'connector', label: translate(IMPLEMENTATION_TYPE_CONNECTOR_LABEL)}); - } + const connector = createElement( + "camunda:Connector", + {}, + extensionElements, + bpmnFactory + ); - // add deployment - if (isDeploymentCapable(businessObject)) { - options.push({value: 'deploymentModel', label: translate(IMPLEMENTATION_TYPE_DEPLOYMENT_LABEL)}); - } + commands.push({ + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: extensionElements, + properties: { + values: [...extensionElements.get("values"), connector], + }, + }, + }); + } + } + + // (5) deployment + if (isDeploymentCapable(businessObject)) { + updatedProperties = { + ...updatedProperties, + "opentosca:deploymentModelUrl": undefined, + }; + + if (value === "deploymentModel") { + updatedProperties = { + ...updatedProperties, + "opentosca:deploymentModelUrl": "", + }; + } + } + + // (5) collect all property updates + commands.push( + UpdateModdlePropertiesCommand(element, businessObject, updatedProperties) + ); + + // (6) commit all updates + commandStack.execute("properties-panel.multi-command-executor", commands); + }; + + const getOptions = () => { + const businessObject = getServiceTaskLikeBusinessObject(element); + + const options = [ + { value: "", label: translate(IMPLEMENTATION_TYPE_NONE_LABEL) }, + { value: "class", label: translate(IMPLEMENTATION_TYPE_JAVA_LABEL) }, + { + value: "expression", + label: translate(IMPLEMENTATION_TYPE_EXPRESSION_LABEL), + }, + { + value: "delegateExpression", + label: translate(IMPLEMENTATION_TYPE_DELEGATE_LABEL), + }, + ]; - return sortByPriority(options); - }; - - return SelectEntry({ - element, - id: 'implementationType', - label: translate('Type'), - getValue, - setValue, - getOptions - }); + if (isDmnCapable(businessObject)) { + options.push({ + value: "dmn", + label: translate(IMPLEMENTATION_TYPE_DMN_LABEL), + }); + } + + if (isExternalCapable(businessObject)) { + options.push({ + value: "external", + label: translate(IMPLEMENTATION_TYPE_EXTERNAL_LABEL), + }); + } + + if (isServiceTaskLike(businessObject)) { + options.push({ + value: "connector", + label: translate(IMPLEMENTATION_TYPE_CONNECTOR_LABEL), + }); + } + + // add deployment + if (isDeploymentCapable(businessObject)) { + options.push({ + value: "deploymentModel", + label: translate(IMPLEMENTATION_TYPE_DEPLOYMENT_LABEL), + }); + } + + return sortByPriority(options); + }; + + return SelectEntry({ + element, + id: "implementationType", + label: translate("Type"), + getValue, + setValue, + getOptions, + }); } - // helper /////////////////////// function isDelegateType(type) { - return ['class', 'expression', 'delegateExpression'].includes(type); + return ["class", "expression", "delegateExpression"].includes(type); } function getConnectors(businessObject) { - return getExtensionElementsList(businessObject, 'camunda:Connector'); + return getExtensionElementsList(businessObject, "camunda:Connector"); } function UpdateModdlePropertiesCommand(element, businessObject, newProperties) { - return { - cmd: 'element.updateModdleProperties', - context: { - element, - moddleElement: businessObject, - properties: newProperties - } - }; + return { + cmd: "element.updateModdleProperties", + context: { + element, + moddleElement: businessObject, + properties: newProperties, + }, + }; } function sortByPriority(options) { - const priorities = { - [IMPLEMENTATION_TYPE_NONE_LABEL]: 0, - [IMPLEMENTATION_TYPE_JAVA_LABEL]: 3, - [IMPLEMENTATION_TYPE_EXPRESSION_LABEL]: 4, - [IMPLEMENTATION_TYPE_DELEGATE_LABEL]: 5, - [IMPLEMENTATION_TYPE_DMN_LABEL]: 1, - [IMPLEMENTATION_TYPE_EXTERNAL_LABEL]: 2, - [IMPLEMENTATION_TYPE_CONNECTOR_LABEL]: 6 - }; - - return sortBy(options, o => priorities[o.label]); + const priorities = { + [IMPLEMENTATION_TYPE_NONE_LABEL]: 0, + [IMPLEMENTATION_TYPE_JAVA_LABEL]: 3, + [IMPLEMENTATION_TYPE_EXPRESSION_LABEL]: 4, + [IMPLEMENTATION_TYPE_DELEGATE_LABEL]: 5, + [IMPLEMENTATION_TYPE_DMN_LABEL]: 1, + [IMPLEMENTATION_TYPE_EXTERNAL_LABEL]: 2, + [IMPLEMENTATION_TYPE_CONNECTOR_LABEL]: 6, + }; + + return sortBy(options, (o) => priorities[o.label]); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js index 0fa95dff..9a1bdc4a 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/ServiceTaskPropertiesProvider.js @@ -1,6 +1,6 @@ -import {ImplementationProps} from "./ImplementationProps"; -import {Group} from "@bpmn-io/properties-panel"; -import {getWineryEndpoint} from '../../framework-config/config-manager'; +import { ImplementationProps } from "./ImplementationProps"; +import { Group } from "@bpmn-io/properties-panel"; +import { getWineryEndpoint } from "../../framework-config/config-manager"; const LOW_PRIORITY = 500; @@ -9,48 +9,45 @@ const LOW_PRIORITY = 500; * * @param propertiesPanel * @param injector - * @param {Function} translate - * @param eventBus */ -export default function ServiceTaskPropertiesProvider(propertiesPanel, injector, translate, eventBus) { - // subscribe to config updates to retrieve the currently defined Winery endpoint - const self = this; - let wineryEndpoint; - eventBus.on('config.updated', function (config) { - wineryEndpoint = config.wineryEndpoint; - }); - +export default function ServiceTaskPropertiesProvider( + propertiesPanel, + injector +) { + /** + * Return the groups provided for the given element. + * + * @param element + * + * @return {(Object[]) => (Object[])} groups middleware + */ + this.getGroups = function (element) { /** - * Return the groups provided for the given element. + * We return a middleware that modifies + * the existing groups. * - * @param element + * @param {Object[]} groups * - * @return {(Object[]) => (Object[])} groups middleware + * @return {Object[]} modified groups */ - this.getGroups = function (element) { - - /** - * We return a middleware that modifies - * the existing groups. - * - * @param {Object[]} groups - * - * @return {Object[]} modified groups - */ - return function (groups) { - // update ServiceTasks with the deployment extension - if (element.type && element.type === 'bpmn:ServiceTask') { - groups[2] = ImplementationGroup(element, injector, getWineryEndpoint()); - } - return groups; - }; + return function (groups) { + // update ServiceTasks with the deployment extension + if (element.type && element.type === "bpmn:ServiceTask") { + groups[2] = ImplementationGroup(element, injector, getWineryEndpoint()); + } + return groups; }; + }; - propertiesPanel.registerProvider(LOW_PRIORITY, this); + propertiesPanel.registerProvider(LOW_PRIORITY, this); } -ServiceTaskPropertiesProvider.$inject = ['propertiesPanel', 'injector', 'translate', 'eventBus']; - +ServiceTaskPropertiesProvider.$inject = [ + "propertiesPanel", + "injector", + "translate", + "eventBus", +]; /** * Properties group to show customized implementation options entry for service tasks. @@ -62,20 +59,18 @@ ServiceTaskPropertiesProvider.$inject = ['propertiesPanel', 'injector', 'transla * @constructor */ function ImplementationGroup(element, injector, wineryEndpoint) { - const translate = injector.get('translate'); + const translate = injector.get("translate"); - const group = { - label: translate('Implementation'), - id: 'CamundaPlatform__Implementation', - component: Group, - entries: [ - ...ImplementationProps({element, wineryEndpoint, translate}) - ] - }; + const group = { + label: translate("Implementation"), + id: "CamundaPlatform__Implementation", + component: Group, + entries: [...ImplementationProps({ element, wineryEndpoint, translate })], + }; - if (group.entries.length) { - return group; - } + if (group.entries.length) { + return group; + } - return null; + return null; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js index 2ad48048..ef403e57 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlModal.js @@ -10,10 +10,10 @@ */ /* eslint-disable no-unused-vars */ -import React, { useState } from 'react'; -import Modal from '../../../../editor/ui/modal/Modal'; -import './yaml-modal.css'; -import '../../../../editor/config/config-modal.css'; +import React, { useState } from "react"; +import Modal from "../../../../editor/ui/modal/Modal"; +import "./yaml-modal.css"; +import "../../../../editor/config/config-modal.css"; // polyfill upcoming structural components const Title = Modal.Title; @@ -28,62 +28,74 @@ const Footer = Modal.Footer; * @constructor */ export default function YamlModal(props) { - const [uploadFile, setUploadFile] = useState(null); + const [uploadFile, setUploadFile] = useState(null); - const { onClose, element, commandStack } = props; + const { onClose, element, commandStack } = props; - const onSubmit = async () => { - // Process the uploaded file or text input here - console.log('Uploaded file:', uploadFile); - var reader = new FileReader(); - reader.onload = function () { - var fileContent = reader.result; - element.businessObject.yaml = fileContent; - commandStack.execute('element.updateModdleProperties', { - element, - moddleElement: element.businessObject, - properties: { - 'yaml': fileContent - } - }); - }; - reader.readAsText(uploadFile); - // Call close callback - onClose(); + const onSubmit = async () => { + // Process the uploaded file or text input here + console.log("Uploaded file:", uploadFile); + var reader = new FileReader(); + reader.onload = function () { + var fileContent = reader.result; + element.businessObject.yaml = fileContent; + commandStack.execute("element.updateModdleProperties", { + element, + moddleElement: element.businessObject, + properties: { + yaml: fileContent, + }, + }); }; + reader.readAsText(uploadFile); + // Call close callback + onClose(); + }; - return ( - - Upload YML + return ( + + Upload YML - - - - - - - - -
File - { setUploadFile(e.target.files[0]); }} - /> -
+ + + + + + + + +
File + { + setUploadFile(e.target.files[0]); + }} + /> +
+ - - -
-
- - -
-
-
- ); -} \ No newline at end of file +
+
+ + +
+
+
+ ); +} diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js index 8d56b51a..1bc556a6 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/YamlUpload.js @@ -1,30 +1,36 @@ -import { HeaderButton } from '@bpmn-io/properties-panel'; -import { useService } from 'bpmn-js-properties-panel'; -import React from 'react'; -import YamlModal from './YamlModal'; -import { createRoot } from 'react-dom/client'; -import './yaml-modal.css'; +import { HeaderButton } from "@bpmn-io/properties-panel"; +import { useService } from "bpmn-js-properties-panel"; +import React from "react"; +import YamlModal from "./YamlModal"; +import { createRoot } from "react-dom/client"; +import "./yaml-modal.css"; /** * Entry to display the button which opens the Yaml Model, a dialog which allows to upload yml files. */ export function YamlUpload(props) { - const { element } = props; - const translate = useService('translate'); - const commandStack = useService('commandStack'); + const { element } = props; + const translate = useService("translate"); + const commandStack = useService("commandStack"); - const onClick = () => { - const root = createRoot(document.getElementById("modal-container")); - root.render( root.unmount()} element={element} commandStack={commandStack}/>); - }; + const onClick = () => { + const root = createRoot(document.getElementById("modal-container")); + root.render( + root.unmount()} + element={element} + commandStack={commandStack} + /> + ); + }; - return HeaderButton({ - element, - id: 'upload-yaml-button', - text: translate('Upload YAML'), - description: 'Upload YML', - className: "upload-yaml-button", - children: 'Upload YAML', - onClick, - }); + return HeaderButton({ + element, + id: "upload-yaml-button", + text: translate("Upload YAML"), + description: "Upload YML", + className: "upload-yaml-button", + children: "Upload YAML", + onClick, + }); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css index 1826a029..750b40c4 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css +++ b/components/bpmn-q/modeler-component/extensions/opentosca/modeling/properties-provider/artifact-modal.css @@ -1,110 +1,108 @@ .tab { - flex: 1; - padding: 10px; - border-radius: 3px; - text-align: center; - cursor: pointer; - font-weight: bold; - font-stretch: normal; - font-style: normal; - line-height: normal; - letter-spacing: normal; - color: #fdfdfe; - margin: 1px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2); - border: solid 1px var(--blue-darken-62); - background-color: var(--blue-base-65); + flex: 1; + padding: 10px; + border-radius: 3px; + text-align: center; + cursor: pointer; + font-weight: bold; + font-stretch: normal; + font-style: normal; + line-height: normal; + letter-spacing: normal; + color: #fdfdfe; + margin: 1px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2); + border: solid 1px var(--blue-darken-62); + background-color: var(--blue-base-65); } .tab.active { - background-color: var(--blue-darken-55); + background-color: var(--blue-darken-55); } .tab:hover { - border: solid 1px var(--blue-darken-55); - background-color: var(--blue-darken-62); + border: solid 1px var(--blue-darken-55); + background-color: var(--blue-darken-62); } .tab-buttons-container { - display: flex; - flex-direction: row; - align-items: center; - overflow: auto; - min-width: 120px; - max-height: 263px; - direction: ltr; + display: flex; + flex-direction: row; + align-items: center; + overflow: auto; + min-width: 120px; + max-height: 263px; + direction: ltr; } - .qwm-artifact-upload-btn { - outline: none; - background-color: var(--color-grey-225-10-97); - border: 1px solid var(--color-grey-225-10-75); - border-radius: 4px; - margin: 2px 32px 6px 12px; - padding: 2px 6px 2px 6px; - fill: var(--add-entry-fill-color); + outline: none; + background-color: var(--color-grey-225-10-97); + border: 1px solid var(--color-grey-225-10-75); + border-radius: 4px; + margin: 2px 32px 6px 12px; + padding: 2px 6px 2px 6px; + fill: var(--add-entry-fill-color); } .qwm-artifact-upload-btn:hover { - color: white; - border: 1px solid white; - background-color: var(--color-blue-205-100-50); - fill: var(--add-entry-hover-fill-color); + color: white; + border: 1px solid white; + background-color: var(--color-blue-205-100-50); + fill: var(--add-entry-hover-fill-color); } .upload-tab-content { - display: flex; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - padding: 3px 6px 2px; - column-gap: 10px; + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + padding: 3px 6px 2px; + column-gap: 10px; } .dockerimage-input { - flex-grow: 2; + flex-grow: 2; } .upload-properties-panel-input { - padding: 3px 6px 2px; - border: 1px solid hsl(225, 10%, 75%); - border-radius: 2px; - background-color: rgb(247, 247, 248); - font-size: 14px; - font-family: inherit; + padding: 3px 6px 2px; + border: 1px solid hsl(225, 10%, 75%); + border-radius: 2px; + background-color: rgb(247, 247, 248); + font-size: 14px; + font-family: inherit; } .upload-properties-panel-label { - display: block; - font-size: var(--text-size-small); - margin: 2px 0 1px; + display: block; + font-size: var(--text-size-small); + margin: 2px 0 1px; } .upload-artifact-tab { - display: flex; - flex-direction: row; - align-items: center; - padding: 3px 6px 2px; - font-size: 14px; - font-family: inherit; - width: 100% + display: flex; + flex-direction: row; + align-items: center; + padding: 3px 6px 2px; + font-size: 14px; + font-family: inherit; + width: 100%; } .upload-artifact-selector { - flex: 1; - max-width: 282px; + flex: 1; + max-width: 282px; } .upload-file-upload { - flex: 1; - max-width: 282px; - overflow: clip; - text-overflow: ellipsis; + flex: 1; + max-width: 282px; + overflow: clip; + text-overflow: ellipsis; } .upload-file-upload-button { - overflow: hidden; - text-overflow: ellipsis; + overflow: hidden; + text-overflow: ellipsis; } - diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js index 9d3b0489..0bb60736 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/replacement/OnDemandTransformator.js @@ -9,15 +9,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {createTempModelerFromXml} from '../../../editor/ModelerHandler'; -import {getXml} from '../../../editor/util/IoUtilities'; -import {isDeployableServiceTask} from "../deployment/DeploymentUtils"; +import { createTempModelerFromXml } from "../../../editor/ModelerHandler"; +import { getXml } from "../../../editor/util/IoUtilities"; +import { isDeployableServiceTask } from "../deployment/DeploymentUtils"; import * as config from "../framework-config/config-manager"; -import {makeId} from "../deployment/OpenTOSCAUtils"; -import {getCamundaEndpoint} from "../../../editor/config/EditorConfigManager"; -import {createElement} from "../../../editor/util/camunda-utils/ElementUtil"; -import {useService} from "bpmn-js-properties-panel"; - +import { makeId } from "../deployment/OpenTOSCAUtils"; +import { getCamundaEndpoint } from "../../../editor/config/EditorConfigManager"; +import { createElement } from "../../../editor/util/camunda-utils/ElementUtil"; const fetchMethod = ` function fetch(method, url, body) { @@ -57,7 +55,7 @@ function fetch(method, url, body) { }`; function createDeploymentScript(params) { - return ` + return ` var params = ${JSON.stringify(params)}; params.csarName = "ondemand_" + (Math.random().toString().substring(3)); @@ -90,7 +88,7 @@ execution.setVariable(params.subprocessId + "_deploymentBuildPlanInstanceUrl", b } function createWaitScript(params) { - return ` + return ` var params = ${JSON.stringify(params)}; ${fetchMethod} @@ -134,97 +132,135 @@ java.lang.Thread.sleep(12000); * @param endpointConfig endpoints of the services required for the dynamic hardware selection */ export async function startOnDemandReplacementProcess(xml) { - const modeler = await createTempModelerFromXml(xml); - const modeling = modeler.get('modeling'); - const elementRegistry = modeler.get('elementRegistry'); - const bpmnReplace = modeler.get('bpmnReplace'); - const bpmnAutoResizeProvider = modeler.get('bpmnAutoResizeProvider'); - const bpmnFactory = modeler.get('bpmnFactory'); - bpmnAutoResizeProvider.canResize = () => false; - - const serviceTasks = elementRegistry.filter(({businessObject}) => isDeployableServiceTask(businessObject)); - - for (const serviceTask of serviceTasks) { - let deploymentModelUrl = serviceTask.businessObject.get('opentosca:deploymentModelUrl'); - if (deploymentModelUrl.startsWith('{{ wineryEndpoint }}')) { - deploymentModelUrl = deploymentModelUrl.replace('{{ wineryEndpoint }}', config.getWineryEndpoint()); - } + const modeler = await createTempModelerFromXml(xml); + const modeling = modeler.get("modeling"); + const elementRegistry = modeler.get("elementRegistry"); + const bpmnReplace = modeler.get("bpmnReplace"); + const bpmnAutoResizeProvider = modeler.get("bpmnAutoResizeProvider"); + const bpmnFactory = modeler.get("bpmnFactory"); + bpmnAutoResizeProvider.canResize = () => false; + + const serviceTasks = elementRegistry.filter(({ businessObject }) => + isDeployableServiceTask(businessObject) + ); + + for (const serviceTask of serviceTasks) { + let deploymentModelUrl = serviceTask.businessObject.get( + "opentosca:deploymentModelUrl" + ); + if (deploymentModelUrl.startsWith("{{ wineryEndpoint }}")) { + deploymentModelUrl = deploymentModelUrl.replace( + "{{ wineryEndpoint }}", + config.getWineryEndpoint() + ); + } + + const extensionElements = serviceTask.businessObject.extensionElements; + + let subProcess = bpmnReplace.replaceElement(serviceTask, { + type: "bpmn:SubProcess", + }); - const extensionElements = serviceTask.businessObject.extensionElements; - - let subProcess = bpmnReplace.replaceElement(serviceTask, {type: 'bpmn:SubProcess'}); - - subProcess.businessObject.set("opentosca:onDemandDeployment", true); - subProcess.businessObject.set("opentosca:deploymentModelUrl", deploymentModelUrl); - - const startEvent = modeling.createShape({ - type: 'bpmn:StartEvent' - }, {x: 200, y: 200}, subProcess); - - let topicName = makeId(12); - const serviceTask1 = modeling.appendShape(startEvent, { - type: 'bpmn:ScriptTask' - }, {x: 400, y: 200}); - serviceTask1.businessObject.set("scriptFormat", "javascript"); - serviceTask1.businessObject.set("script", createDeploymentScript( - { - opentoscaEndpoint: config.getOpenTOSCAEndpoint(), - deploymentModelUrl: deploymentModelUrl, - subprocessId: subProcess.id, - camundaTopic: topicName, - camundaEndpoint: getCamundaEndpoint() - } - )); - serviceTask1.businessObject.set("name", "Create deployment"); - - const serviceTask2 = modeling.appendShape(serviceTask1, { - type: 'bpmn:ScriptTask', - }, {x: 600, y: 200}); - serviceTask2.businessObject.set("scriptFormat", "javascript"); - serviceTask2.businessObject.set("script", createWaitScript( - {subprocessId: subProcess.id} - )); - serviceTask2.businessObject.set("name", "Wait for deployment"); - - const serviceTask3 = modeling.appendShape(serviceTask2, { - type: 'bpmn:ServiceTask', - }, {x: 800, y: 200}); - - serviceTask3.businessObject.set("name", "Call service"); - if (!extensionElements) { - serviceTask3.businessObject.set("camunda:type", "external"); - serviceTask3.businessObject.set("camunda:topic", topicName); - - } else { - const values = extensionElements.values; - for (let value of values) { - if (value.inputOutput === undefined) continue; - for (let param of value.inputOutput.inputParameters) { - if (param.name === "url") { - param.value = `\${selfserviceApplicationUrl.concat(${JSON.stringify(param.value || "")})}`; - break; - } - } - } - - const newExtensionElements = createElement( - 'bpmn:ExtensionElements', - {values}, - serviceTask2.businessObject, - bpmnFactory - ); - subProcess.businessObject.set("extensionElements", undefined); - serviceTask3.businessObject.set("extensionElements", newExtensionElements); + subProcess.businessObject.set("opentosca:onDemandDeployment", true); + subProcess.businessObject.set( + "opentosca:deploymentModelUrl", + deploymentModelUrl + ); + + const startEvent = modeling.createShape( + { + type: "bpmn:StartEvent", + }, + { x: 200, y: 200 }, + subProcess + ); + + let topicName = makeId(12); + const serviceTask1 = modeling.appendShape( + startEvent, + { + type: "bpmn:ScriptTask", + }, + { x: 400, y: 200 } + ); + serviceTask1.businessObject.set("scriptFormat", "javascript"); + serviceTask1.businessObject.set( + "script", + createDeploymentScript({ + opentoscaEndpoint: config.getOpenTOSCAEndpoint(), + deploymentModelUrl: deploymentModelUrl, + subprocessId: subProcess.id, + camundaTopic: topicName, + camundaEndpoint: getCamundaEndpoint(), + }) + ); + serviceTask1.businessObject.set("name", "Create deployment"); + + const serviceTask2 = modeling.appendShape( + serviceTask1, + { + type: "bpmn:ScriptTask", + }, + { x: 600, y: 200 } + ); + serviceTask2.businessObject.set("scriptFormat", "javascript"); + serviceTask2.businessObject.set( + "script", + createWaitScript({ subprocessId: subProcess.id }) + ); + serviceTask2.businessObject.set("name", "Wait for deployment"); + + const serviceTask3 = modeling.appendShape( + serviceTask2, + { + type: "bpmn:ServiceTask", + }, + { x: 800, y: 200 } + ); + + serviceTask3.businessObject.set("name", "Call service"); + if (!extensionElements) { + serviceTask3.businessObject.set("camunda:type", "external"); + serviceTask3.businessObject.set("camunda:topic", topicName); + } else { + const values = extensionElements.values; + for (let value of values) { + if (value.inputOutput === undefined) continue; + for (let param of value.inputOutput.inputParameters) { + if (param.name === "url") { + param.value = `\${selfserviceApplicationUrl.concat(${JSON.stringify( + param.value || "" + )})}`; + break; + } } - const endTask = modeling.appendShape(serviceTask3, { - type: 'bpmn:EndEvent' - }, {x: 1000, y: 200}, subProcess); + } + const newExtensionElements = createElement( + "bpmn:ExtensionElements", + { values }, + serviceTask2.businessObject, + bpmnFactory + ); + subProcess.businessObject.set("extensionElements", undefined); + serviceTask3.businessObject.set( + "extensionElements", + newExtensionElements + ); } + modeling.appendShape( + serviceTask3, + { + type: "bpmn:EndEvent", + }, + { x: 1000, y: 200 }, + subProcess + ); + } - // layout diagram after successful transformation - let updatedXml = await getXml(modeler); - console.log(updatedXml); + // layout diagram after successful transformation + let updatedXml = await getXml(modeler); + console.log(updatedXml); - return updatedXml; + return updatedXml; } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json b/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json index ee1b10a4..dd216089 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json +++ b/components/bpmn-q/modeler-component/extensions/opentosca/resources/opentosca4bpmn.json @@ -1,28 +1,28 @@ { - "name": "OpenTOSCA4BPMN", - "uri": "https://github.com/UST-QuAntiL/OpenTOSCA", - "prefix": "opentosca", - "xml": { - "tagAlias": "lowerCase" - }, - "types": [ - { - "name": "ServiceTask", - "extends": [ "bpmn:ServiceTask" ], - "properties": [ - { - "name": "deploymentModelUrl", - "isAttr": true, - "type": "String" - }, - { - "name": "connectorUrl", - "isAttr": true, - "type": "String" - } - ] - } - ], - "enumerations": [], - "associations": [] + "name": "OpenTOSCA4BPMN", + "uri": "https://github.com/UST-QuAntiL/OpenTOSCA", + "prefix": "opentosca", + "xml": { + "tagAlias": "lowerCase" + }, + "types": [ + { + "name": "ServiceTask", + "extends": ["bpmn:ServiceTask"], + "properties": [ + { + "name": "deploymentModelUrl", + "isAttr": true, + "type": "String" + }, + { + "name": "connectorUrl", + "isAttr": true, + "type": "String" + } + ] + } + ], + "enumerations": [], + "associations": [] } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css b/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css index be326f0e..b01ed139 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css +++ b/components/bpmn-q/modeler-component/extensions/opentosca/styling/opentosca.css @@ -1,127 +1,125 @@ -@import url('~bpmn-font/dist/css/bpmn-embedded.css'); +@import url("~bpmn-font/dist/css/bpmn-embedded.css"); .qwm .config:before { - content: ""; - display: block; - width: 15px; - height: 15px; - background-size: contain; - background: url("../resources/config-icon.png") no-repeat center center; - float: left; + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../resources/config-icon.png") no-repeat center center; + float: left; } .qwm .app-icon-opentosca:before { - content: ""; - width: 15px; - height: 15px; - background-image: url("../resources/opentosca-icon.png"); - background-size: contain; - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 15px; + height: 15px; + background-image: url("../resources/opentosca-icon.png"); + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + float: left; } .qwm .app-icon-service-deployment:before { - content: ""; - width: 15px; - height: 15px; - background-image: url("../resources/service-deployment-icon.png"); - background-size: contain; - background-repeat: no-repeat; - display: inline-block; - float: left; + content: ""; + width: 15px; + height: 15px; + background-image: url("../resources/service-deployment-icon.png"); + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + float: left; } .qwm .indent { - margin-left: 5px; + margin-left: 5px; } .qwm .spaceUnder { - padding-bottom: 1em; + padding-bottom: 1em; } .qwm .spaceUnderSmall { - padding-bottom: 0.3em; + padding-bottom: 0.3em; } .qwm .spaceAbove { - padding-top: 1em; + padding-top: 1em; } .qwm .hidden { - display: none; + display: none; } .qwm .djs-label { - font-family: 'Arial', sans-serif; + font-family: "Arial", sans-serif; } .qwm .adaptation-tab { - overflow: hidden; - border: 1px solid #ccc; - background-color: #f1f1f1; + overflow: hidden; + border: 1px solid #ccc; + background-color: #f1f1f1; } /* Style the buttons that are used to open the tab content */ .qwm .adaptation-tab-button { - background-color: inherit; - float: left; - border: none; - outline: none; - cursor: pointer; - transition: 0.3s; - padding: 14px 16px; + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + transition: 0.3s; + padding: 14px 16px; } /* Change background color of buttons on hover */ .qwm .adaptation-tab-button:hover { - background-color: #ddd; + background-color: #ddd; } /* Create an active/current tablink class */ .qwm .adaptation-tab-button.active { - background-color: #ccc; + background-color: #ccc; } .qwm .rewrite-failed-button:after { - content: ""; - display: block; - width: 15px; - height: 15px; - background-size: contain; - background: url("../resources/info.png") no-repeat center center; - float: right; - padding-left: 20px; + content: ""; + display: block; + width: 15px; + height: 15px; + background-size: contain; + background: url("../resources/info.png") no-repeat center center; + float: right; + padding-left: 20px; } -.qwm .rewrite-failed-button:disabled{ - background-color: #f44336; - color: #000000; +.qwm .rewrite-failed-button:disabled { + background-color: #f44336; + color: #000000; } -.qwm .rewrite-successful-button:disabled{ - background-color: #008000; - color: #000000; +.qwm .rewrite-successful-button:disabled { + background-color: #008000; + color: #000000; } .qwm .show-icon:before { - content: ""; - display: block; - width: 16px; - height: 16px; - background: url("../resources/show-icon.png") no-repeat center center; - background-size: contain; - float: left; + content: ""; + display: block; + width: 16px; + height: 16px; + background: url("../resources/show-icon.png") no-repeat center center; + background-size: contain; + float: left; } - .qwm .hide-icon:before { - content: ""; - display: block; - width: 16px; - height: 16px; - background: url("../resources/hide-icon.png") no-repeat center center; - background-size: contain; - float: left; + content: ""; + display: block; + width: 16px; + height: 16px; + background: url("../resources/hide-icon.png") no-repeat center center; + background-size: contain; + float: left; } - diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js index c7906d7c..79ebf70e 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/DeploymentPlugin.js @@ -10,344 +10,394 @@ */ /* eslint-disable no-unused-vars*/ -import React, {Fragment, PureComponent} from 'react'; - -import ServiceDeploymentOverviewModal from './ServiceDeploymentOverviewModal'; -import ServiceDeploymentInputModal from './ServiceDeploymentInputModal'; -import ServiceDeploymentBindingModal from './ServiceDeploymentBindingModal'; - -import {createServiceInstance, uploadCSARToContainer} from '../../../deployment/OpenTOSCAUtils'; -import {bindUsingPull, bindUsingPush} from '../../../deployment/BindingUtils'; -import {getServiceTasksToDeploy} from '../../../deployment/DeploymentUtils'; -import {getModeler} from "../../../../../editor/ModelerHandler"; +import React, { Fragment, PureComponent } from "react"; + +import ServiceDeploymentOverviewModal from "./ServiceDeploymentOverviewModal"; +import ServiceDeploymentInputModal from "./ServiceDeploymentInputModal"; +import ServiceDeploymentBindingModal from "./ServiceDeploymentBindingModal"; + +import { + createServiceInstance, + uploadCSARToContainer, +} from "../../../deployment/OpenTOSCAUtils"; +import { bindUsingPull, bindUsingPush } from "../../../deployment/BindingUtils"; +import { getServiceTasksToDeploy } from "../../../deployment/DeploymentUtils"; +import { getModeler } from "../../../../../editor/ModelerHandler"; import NotificationHandler from "../../../../../editor/ui/notifications/NotificationHandler"; -import {getRootProcess} from '../../../../../editor/util/ModellingUtilities'; +import { getRootProcess } from "../../../../../editor/util/ModellingUtilities"; import ExtensibleButton from "../../../../../editor/ui/ExtensibleButton"; const defaultState = { - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, }; export default class DeploymentPlugin extends PureComponent { - - constructor(props) { - super(props); - - this.state = defaultState; - - this.handleDeploymentOverviewClosed = this.handleDeploymentOverviewClosed.bind(this); - this.handleDeploymentInputClosed = this.handleDeploymentInputClosed.bind(this); - this.handleDeploymentBindingClosed = this.handleDeploymentBindingClosed.bind(this); + constructor(props) { + super(props); + + this.state = defaultState; + + this.handleDeploymentOverviewClosed = + this.handleDeploymentOverviewClosed.bind(this); + this.handleDeploymentInputClosed = + this.handleDeploymentInputClosed.bind(this); + this.handleDeploymentBindingClosed = + this.handleDeploymentBindingClosed.bind(this); + } + + componentDidMount() { + this.modeler = getModeler(); + this.commandStack = this.modeler.get("commandStack"); + } + + /** + * Increase the progress in the progress bar + * + * @param progressBar the progress bar to handle + * @param progress the percentage to increase the current progress + */ + handleProgress(progressBar, progress) { + if (!progressBar.innerHTML) { + progressBar.innerHTML = "0%"; } - componentDidMount() { - this.modeler = getModeler(); - this.commandStack = this.modeler.get("commandStack"); + let currentWidth = parseInt(progressBar.innerHTML.replace(/% ?/g, "")); + for (let i = 0; i < progress; i++) { + currentWidth++; + progressBar.style.width = currentWidth + "%"; + progressBar.innerHTML = currentWidth + "%"; } - - /** - * Increase the progress in the progress bar - * - * @param progressBar the progress bar to handle - * @param progress the percentage to increase the current progress - */ - handleProgress(progressBar, progress) { - if (!progressBar.innerHTML) { - progressBar.innerHTML = '0%'; + } + + /** + * Handle the result of a close operation on the deployment overview modal + * + * @param result the result from the close operation + */ + async handleDeploymentOverviewClosed(result) { + // handle click on 'Next' button + if (result && result.hasOwnProperty("next") && result.next === true) { + // make progress bar visible and hide buttons + result.refs.progressBarDivRef.current.hidden = false; + result.refs.footerRef.current.hidden = true; + let progressBar = result.refs.progressBarRef.current; + this.handleProgress(progressBar, 10); + + // calculate progress step size for the number of CSARs to deploy + let csarList = result.csarList; + let progressStep = Math.round(90 / csarList.length); + + // upload all CSARs + for (let i = 0; i < csarList.length; i++) { + let csar = csarList[i]; + console.log("Uploading CSAR to OpenTOSCA container: ", csar); + + let uploadResult = await uploadCSARToContainer( + this.modeler.config.opentoscaEndpoint, + csar.csarName, + csar.url, + this.modeler.config.wineryEndpoint + ); + if (uploadResult.success === false) { + // notify user about failed CSAR upload + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Unable to upload CSAR to the OpenTOSCA Container", + content: + "CSAR defined for ServiceTasks with Id '" + + csar.serviceTaskIds + + "' could not be uploaded to the connected OpenTOSCA Container!", + duration: 20000, + }); + + // abort process + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + }); + return; } - let currentWidth = parseInt(progressBar.innerHTML.replace(/% ?/g, '')); - for (let i = 0; i < progress; i++) { - currentWidth++; - progressBar.style.width = currentWidth + '%'; - progressBar.innerHTML = currentWidth + '%'; - } - } + // set URL of the CSAR in the OpenTOSCA Container which is required to create instances + csar.buildPlanUrl = uploadResult.url; + csar.inputParameters = uploadResult.inputParameters; - /** - * Handle the result of a close operation on the deployment overview modal - * - * @param result the result from the close operation - */ - async handleDeploymentOverviewClosed(result) { - - // handle click on 'Next' button - if (result && result.hasOwnProperty('next') && result.next === true) { - - // make progress bar visible and hide buttons - result.refs.progressBarDivRef.current.hidden = false; - result.refs.footerRef.current.hidden = true; - let progressBar = result.refs.progressBarRef.current; - this.handleProgress(progressBar, 10); - - // calculate progress step size for the number of CSARs to deploy - let csarList = result.csarList; - let progressStep = Math.round(90 / csarList.length); - - // upload all CSARs - for (let i = 0; i < csarList.length; i++) { - let csar = csarList[i]; - console.log('Uploading CSAR to OpenTOSCA container: ', csar); - - let uploadResult = await uploadCSARToContainer(this.modeler.config.opentoscaEndpoint, csar.csarName, csar.url, this.modeler.config.wineryEndpoint); - if (uploadResult.success === false) { - - // notify user about failed CSAR upload - NotificationHandler.getInstance().displayNotification({ - type: 'error', - title: 'Unable to upload CSAR to the OpenTOSCA Container', - content: 'CSAR defined for ServiceTasks with Id \'' + csar.serviceTaskIds + '\' could not be uploaded to the connected OpenTOSCA Container!', - duration: 20000 - }); - - // abort process - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - return; - } - - // set URL of the CSAR in the OpenTOSCA Container which is required to create instances - csar.buildPlanUrl = uploadResult.url; - csar.inputParameters = uploadResult.inputParameters; - - // increase progress in the UI - this.handleProgress(progressBar, progressStep); - } - - this.csarList = csarList; + // increase progress in the UI + this.handleProgress(progressBar, progressStep); + } - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: true, - windowOpenDeploymentBinding: false, - csarList: csarList - }); - return; - } + this.csarList = csarList; - // handle cancellation - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: true, + windowOpenDeploymentBinding: false, + csarList: csarList, + }); + return; } - /** - * Handle the result of a close operation on the deployment input modal - * - * @param result the result from the close operation - */ - async handleDeploymentInputClosed(result) { - - // handle click on 'Next' button - if (result && result.hasOwnProperty('next') && result.next === true) { - - // make progress bar visible and hide buttons - result.refs.progressBarDivRef.current.hidden = false; - result.refs.footerRef.current.hidden = true; - let progressBar = result.refs.progressBarRef.current; - this.handleProgress(progressBar, 10); - - // calculate progress step size for the number of CSARs to create an service instance for - let csarList = result.csarList; - let progressStep = Math.round(90 / csarList.length); - - // create service instances for all CSARs - for (let i = 0; i < csarList.length; i++) { - let csar = csarList[i]; - console.log('Creating service instance for CSAR: ', csar); - - let instanceCreationResponse = await createServiceInstance(csar, this.modeler.config.camundaEndpoint); - csar.properties = instanceCreationResponse.properties; - if (instanceCreationResponse.success === false) { - - // notify user about failed instance creation - NotificationHandler.getInstance().displayNotification({ - type: 'error', - title: 'Unable to create service instace', - content: 'Unable to create service instance for CSAR \'' + csar.csarName + '\'. Aborting process!', - duration: 20000 - }); - - // abort process - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - return; - } - - // store topic name for pulling services - if (instanceCreationResponse.topicName !== undefined) { - csar.topicName = instanceCreationResponse.topicName; - } - - // increase progress in the UI - this.handleProgress(progressBar, progressStep); - } - - // update CSAR list for the binding - this.csarList = csarList; - - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: true, - }); - return; - } - - // handle cancellation - this.setState({ + // handle cancellation + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + }); + } + + /** + * Handle the result of a close operation on the deployment input modal + * + * @param result the result from the close operation + */ + async handleDeploymentInputClosed(result) { + // handle click on 'Next' button + if (result && result.hasOwnProperty("next") && result.next === true) { + // make progress bar visible and hide buttons + result.refs.progressBarDivRef.current.hidden = false; + result.refs.footerRef.current.hidden = true; + let progressBar = result.refs.progressBarRef.current; + this.handleProgress(progressBar, 10); + + // calculate progress step size for the number of CSARs to create an service instance for + let csarList = result.csarList; + let progressStep = Math.round(90 / csarList.length); + + // create service instances for all CSARs + for (let i = 0; i < csarList.length; i++) { + let csar = csarList[i]; + console.log("Creating service instance for CSAR: ", csar); + + let instanceCreationResponse = await createServiceInstance( + csar, + this.modeler.config.camundaEndpoint + ); + csar.properties = instanceCreationResponse.properties; + if (instanceCreationResponse.success === false) { + // notify user about failed instance creation + NotificationHandler.getInstance().displayNotification({ + type: "error", + title: "Unable to create service instace", + content: + "Unable to create service instance for CSAR '" + + csar.csarName + + "'. Aborting process!", + duration: 20000, + }); + + // abort process + this.setState({ windowOpenDeploymentOverview: false, windowOpenDeploymentInput: false, windowOpenDeploymentBinding: false, - }); - } - - /** - * Handle the result of a close operation on the deployment binding modal - * - * @param result the result from the close operation - */ - handleDeploymentBindingClosed(result) { - - // handle click on 'Next' button - if (result && result.hasOwnProperty('next') && result.next === true) { - - // iterate through each CSAR and related ServiceTask and perform the binding with the created service instance - let csarList = result.csarList; - for (let i = 0; i < csarList.length; i++) { - let csar = csarList[i]; - - let serviceTaskIds = csar.serviceTaskIds; - for (let j = 0; j < serviceTaskIds.length; j++) { - - // bind the service instance using the specified binding pattern - let bindingResponse = undefined; - if (csar.type === 'pull') { - bindingResponse = bindUsingPull(csar, serviceTaskIds[j], this.modeler.get('elementRegistry'), this.modeler.get('modeling')); - } else if (csar.type === 'push') { - bindingResponse = bindUsingPush(csar, serviceTaskIds[j], this.modeler.get('elementRegistry'), this.modeler.get('modeling')); - } - - // abort if binding pattern is invalid or binding fails - if (bindingResponse === undefined || bindingResponse.success === false) { - - // notify user about failed binding - NotificationHandler.getInstance().displayNotification({ - type: 'error', - title: 'Unable to perform binding', - content: 'Unable to bind ServiceTask with Id \'' + serviceTaskIds[j] + '\' using binding pattern \'' + csar.type + '\'. Aborting process!', - duration: 20000 - }); - - // abort process - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - return; - } - } - } - - // notify user about successful binding - NotificationHandler.getInstance().displayNotification({ - type: 'info', - title: 'Binding completed', - content: 'Binding of the deployed service instances completed. The resulting workflow can now be deployed to the Camunda engine!', - duration: 20000 - }); + }); + return; } - this.setState({ - windowOpenDeploymentOverview: false, - windowOpenDeploymentInput: false, - windowOpenDeploymentBinding: false, - }); - } + // store topic name for pulling services + if (instanceCreationResponse.topicName !== undefined) { + csar.topicName = instanceCreationResponse.topicName; + } - /** - * Get the list of ServiceTasks to deploy a service for to display them in the modal - */ - getServiceTasksToDeployForModal() { + // increase progress in the UI + this.handleProgress(progressBar, progressStep); + } - if (!this.modeler) { - console.warn('Modeler not available, unable to retrieve ServiceTasks!'); - return []; - } + // update CSAR list for the binding + this.csarList = csarList; - // get all ServiceTasks with associated deployment model - let csarsToDeploy = getServiceTasksToDeploy(getRootProcess(this.modeler.getDefinitions())); + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: true, + }); + return; + } - if (csarsToDeploy.length === 0) { + // handle cancellation + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + }); + } + + /** + * Handle the result of a close operation on the deployment binding modal + * + * @param result the result from the close operation + */ + handleDeploymentBindingClosed(result) { + // handle click on 'Next' button + if (result && result.hasOwnProperty("next") && result.next === true) { + // iterate through each CSAR and related ServiceTask and perform the binding with the created service instance + let csarList = result.csarList; + for (let i = 0; i < csarList.length; i++) { + let csar = csarList[i]; + + let serviceTaskIds = csar.serviceTaskIds; + for (let j = 0; j < serviceTaskIds.length; j++) { + // bind the service instance using the specified binding pattern + let bindingResponse = undefined; + if (csar.type === "pull") { + bindingResponse = bindUsingPull( + csar, + serviceTaskIds[j], + this.modeler.get("elementRegistry"), + this.modeler.get("modeling") + ); + } else if (csar.type === "push") { + bindingResponse = bindUsingPush( + csar, + serviceTaskIds[j], + this.modeler.get("elementRegistry") + ); + } + + // abort if binding pattern is invalid or binding fails + if ( + bindingResponse === undefined || + bindingResponse.success === false + ) { + // notify user about failed binding NotificationHandler.getInstance().displayNotification({ - type: 'info', - title: 'No ServiceTasks with associated deployment models', - content: 'The workflow does not contain ServiceTasks with associated deployment models. No service deployment required!', - duration: 20000 + type: "error", + title: "Unable to perform binding", + content: + "Unable to bind ServiceTask with Id '" + + serviceTaskIds[j] + + "' using binding pattern '" + + csar.type + + "'. Aborting process!", + duration: 20000, }); - } - return csarsToDeploy; + // abort process + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + }); + return; + } + } + } + + // notify user about successful binding + NotificationHandler.getInstance().displayNotification({ + type: "info", + title: "Binding completed", + content: + "Binding of the deployed service instances completed. The resulting workflow can now be deployed to the Camunda engine!", + duration: 20000, + }); } - showDeployment(show) { - this.commandStack.execute("deploymentModel.showAll", { - showDeploymentModel: show - }); + this.setState({ + windowOpenDeploymentOverview: false, + windowOpenDeploymentInput: false, + windowOpenDeploymentBinding: false, + }); + } + + /** + * Get the list of ServiceTasks to deploy a service for to display them in the modal + */ + getServiceTasksToDeployForModal() { + if (!this.modeler) { + console.warn("Modeler not available, unable to retrieve ServiceTasks!"); + return []; } - render() { - // render deployment button and pop-up menu - return ( - this.showDeployment(true)}> - Show Deployment - , - , - ]}/> - {this.state.windowOpenDeploymentOverview && ( - - )} - {this.state.windowOpenDeploymentInput && ( - - )} - {this.state.windowOpenDeploymentBinding && ( - - )} - ); + // get all ServiceTasks with associated deployment model + let csarsToDeploy = getServiceTasksToDeploy( + getRootProcess(this.modeler.getDefinitions()) + ); + + if (csarsToDeploy.length === 0) { + NotificationHandler.getInstance().displayNotification({ + type: "info", + title: "No ServiceTasks with associated deployment models", + content: + "The workflow does not contain ServiceTasks with associated deployment models. No service deployment required!", + duration: 20000, + }); } + + return csarsToDeploy; + } + + showDeployment(show) { + this.commandStack.execute("deploymentModel.showAll", { + showDeploymentModel: show, + }); + } + + render() { + // render deployment button and pop-up menu + return ( + + this.showDeployment(true)} + > + + Show Deployment + + , + , + , + ]} + /> + {this.state.windowOpenDeploymentOverview && ( + + )} + {this.state.windowOpenDeploymentInput && ( + + )} + {this.state.windowOpenDeploymentBinding && ( + + )} + + ); + } } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js index af57d503..6ddc57ab 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentBindingModal.js @@ -10,77 +10,101 @@ */ /* eslint-disable no-unused-vars */ -import React from 'react'; +import React from "react"; // polyfill upcoming structural components import Modal from "../../../../../editor/ui/modal/Modal"; -const Title = Modal.Title || (({children}) =>

{children}

); -const Body = Modal.Body || (({children}) =>
{children}
); -const Footer = Modal.Footer || (({children}) =>
{children}
); - -export default function ServiceDeploymentBindingModal({onClose, initValues}) { - - // find all tasks that have to be bound and assign them to a list based on the used pattern - let bindByPullTasks = []; - let bindByPushTasks = []; - for (let i = 0; i < initValues.length; i++) { - let csar = initValues[i]; - let serviceTaskIds = csar.serviceTaskIds; - for (let j = 0; j < serviceTaskIds.length; j++) { - - if (csar.type === 'pull') { - bindByPullTasks.push(
  • {serviceTaskIds[j]}
  • ); - continue; - } - - if (csar.type === 'push') { - bindByPushTasks.push(
  • {serviceTaskIds[j]}
  • ); - continue; - } - - console.error('Found task that does not use the push or pull pattern: %s', serviceTaskIds[j]); - } +const Title = Modal.Title || (({ children }) =>

    {children}

    ); +const Body = Modal.Body || (({ children }) =>
    {children}
    ); +const Footer = Modal.Footer || (({ children }) =>
    {children}
    ); + +export default function ServiceDeploymentBindingModal({ onClose, initValues }) { + // find all tasks that have to be bound and assign them to a list based on the used pattern + let bindByPullTasks = []; + let bindByPushTasks = []; + for (let i = 0; i < initValues.length; i++) { + let csar = initValues[i]; + let serviceTaskIds = csar.serviceTaskIds; + for (let j = 0; j < serviceTaskIds.length; j++) { + if (csar.type === "pull") { + bindByPullTasks.push( +
  • + {serviceTaskIds[j]} +
  • + ); + continue; + } + + if (csar.type === "push") { + bindByPushTasks.push( +
  • + {serviceTaskIds[j]} +
  • + ); + continue; + } + + console.error( + "Found task that does not use the push or pull pattern: %s", + serviceTaskIds[j] + ); } - - let bindByPull = bindByPullTasks.length > 0; - let bindByPush = bindByPushTasks.length > 0; - - const onFinished = () => onClose({next: true, csarList: initValues}); - - return - - - Service Deployment (3/3) - - - -

    Service instances are created and can be bound to the workflow.

    - - - - - -

    Caution: Thereby, the workflow is adapted.

    - - -
    -
    - - -
    -
    -
    ; + } + + let bindByPull = bindByPullTasks.length > 0; + let bindByPush = bindByPushTasks.length > 0; + + const onFinished = () => onClose({ next: true, csarList: initValues }); + + return ( + + Service Deployment (3/3) + + +

    + Service instances are created and can be bound to the workflow. +

    + + + + + +

    + Caution: Thereby, the workflow is adapted. +

    + + +
    +
    + + +
    +
    +
    + ); } diff --git a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js index 95b7dcdf..1e4f19b4 100644 --- a/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js +++ b/components/bpmn-q/modeler-component/extensions/opentosca/ui/deployment/services/ServiceDeploymentInputModal.js @@ -10,121 +10,154 @@ */ /* eslint-disable no-unused-vars */ -import React from 'react'; +import React from "react"; // polyfill upcoming structural components import Modal from "../../../../../editor/ui/modal/Modal"; -const Title = Modal.Title || (({children}) =>

    {children}

    ); -const Body = Modal.Body || (({children}) =>
    {children}
    ); -const Footer = Modal.Footer || (({children}) =>
    {children}
    ); - -export default function ServiceDeploymentInputModal({onClose, initValues}) { - - // refs to enable changing the state through the plugin - let progressBarRef = React.createRef(); - let progressBarDivRef = React.createRef(); - let footerRef = React.createRef(); - - // propagte updates on dynamically created input fields to corresponding parameter fields - const handleInputChange = (event, csarIndex, paramIndex) => { - initValues[csarIndex].inputParameters[paramIndex].value = event.target.value; - }; - - // determine input parameters that have to be passed by the user - let csarInputParts = []; - let inputRequired = false; - for (let i = 0; i < initValues.length; i++) { - let csar = initValues[i]; - let inputParams = csar.inputParameters; - - let paramsToRetrieve = []; - for (let j = 0; j < inputParams.length; j++) { - let inputParam = inputParams[j]; - - - // skip parameters that are automatically set by the OpenTOSCA Container - if (inputParam.name === 'instanceDataAPIUrl' || inputParam.name === 'CorrelationID' || inputParam.name === 'csarEntrypoint') { - paramsToRetrieve.push({hidden: true, inputParam: inputParam}); - continue; - } - - // skip parameters that are automatically set during service binding - if (inputParam.name === 'camundaTopic' || inputParam.name === 'camundaEndpoint') { - paramsToRetrieve.push({hidden: true, inputParam: inputParam}); - continue; - } - - paramsToRetrieve.push({hidden: false, inputParam: inputParam}); - } - - if (paramsToRetrieve.filter((param) => param.hidden === false).length > 0) { - inputRequired = true; - - // add entries for the parameters - const listItems = paramsToRetrieve.map((param, j) => - - {param.inputParam.name} - - handleInputChange(event, i, j)}/> - - - ); - - // assemble the table - csarInputParts.push( -
    -

    {csar.csarName}:

    - - - - - - - {listItems} - -
    Parameter NameValue
    -
    ); - } +const Title = Modal.Title || (({ children }) =>

    {children}

    ); +const Body = Modal.Body || (({ children }) =>
    {children}
    ); +const Footer = Modal.Footer || (({ children }) =>
    {children}
    ); + +export default function ServiceDeploymentInputModal({ onClose, initValues }) { + // refs to enable changing the state through the plugin + let progressBarRef = React.createRef(); + let progressBarDivRef = React.createRef(); + let footerRef = React.createRef(); + + // propagte updates on dynamically created input fields to corresponding parameter fields + const handleInputChange = (event, csarIndex, paramIndex) => { + initValues[csarIndex].inputParameters[paramIndex].value = + event.target.value; + }; + + // determine input parameters that have to be passed by the user + let csarInputParts = []; + let inputRequired = false; + for (let i = 0; i < initValues.length; i++) { + let csar = initValues[i]; + let inputParams = csar.inputParameters; + + let paramsToRetrieve = []; + for (let j = 0; j < inputParams.length; j++) { + let inputParam = inputParams[j]; + + // skip parameters that are automatically set by the OpenTOSCA Container + if ( + inputParam.name === "instanceDataAPIUrl" || + inputParam.name === "CorrelationID" || + inputParam.name === "csarEntrypoint" + ) { + paramsToRetrieve.push({ hidden: true, inputParam: inputParam }); + continue; + } + + // skip parameters that are automatically set during service binding + if ( + inputParam.name === "camundaTopic" || + inputParam.name === "camundaEndpoint" + ) { + paramsToRetrieve.push({ hidden: true, inputParam: inputParam }); + continue; + } + + paramsToRetrieve.push({ hidden: false, inputParam: inputParam }); } - const onNext = () => onClose({ - next: true, - csarList: initValues, - refs: {progressBarRef: progressBarRef, progressBarDivRef: progressBarDivRef, footerRef: footerRef} + if (paramsToRetrieve.filter((param) => param.hidden === false).length > 0) { + inputRequired = true; + + // add entries for the parameters + const listItems = paramsToRetrieve.map((param, j) => ( + + {param.inputParam.name} + + handleInputChange(event, i, j)} + /> + + + )); + + // assemble the table + csarInputParts.push( +
    +

    {csar.csarName}:

    + + + + + + + {listItems} + +
    Parameter NameValue
    +
    + ); + } + } + + const onNext = () => + onClose({ + next: true, + csarList: initValues, + refs: { + progressBarRef: progressBarRef, + progressBarDivRef: progressBarDivRef, + footerRef: footerRef, + }, }); - return - - - Service Deployment (2/3) - - - -

    CSARs successfully uploaded to the OpenTOSCA Container.

    - - - - - - {csarInputParts} - -